代码语言

知识点思维导图

25 个知识节点

Java(21) - 微服务是什么

读完后,你应能完成以下任务:

  • 绘制“Java(21) - 微服务是什么 / 先看个你熟悉的东西:微前端”的关键对象与数据流,解释“基座只负责在运行时把它们拼起来。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Java(21) - 微服务是什么 / 单体 vs 微服务:到底差在哪”设计正常与异常输入,验证“改起来快,一个仓库全搞定,跨模块调用就是普通函数调用,不用走网络”,输出首个偏差位置与回归测试结果。
  • 实现“Java(21) - 微服务是什么 / 微服务长什么样”的最小代码或配置,检验“注意 demo 的技术栈混搭得很自然:demo-core 是 PHP、demo-notify 是 Go、那一堆 demo-basic / demo-billing 是 Java。”,输出命令、结果与 Diff,并说明不适用边界。

一个大应用拆成一堆能独立部署的小服务,就像 qiankun 把巨石前端拆成一个个能独立上线的子应用——这一课讲清楚 demo 为什么这么拆、怎么拆、拆完之后这些服务又是怎么互相喊话的。


一、先看个你熟悉的东西:微前端

你在前端肯定听过、甚至用过 qiankun / micro-app / Module Federation 这类微前端方案。它解决的问题是:

一个巨大的后台管理系统,几十个菜单、几十个团队往里塞代码,最后变成一个谁都不敢动的 node_modules 巨兽。每次发版,A 团队改个按钮,B、C、D 团队的代码全得一起重新打包、一起上线、一起背锅。

微前端的做法是:

                  ┌─────────────────────────────────┐
                  │        主应用 (qiankun 基座)      │
                  │   负责路由分发、菜单、登录态共享   │
                  └───────┬─────────┬─────────┬───────┘
                          │         │         │
                  ┌───────▼──┐ ┌────▼─────┐ ┌─▼────────┐
                  │ 子应用-订单│ │子应用-财务│ │子应用-车辆│
                  │ React     │ │ Vue2     │ │ Vue3     │
                  │ 独立仓库   │ │ 独立仓库  │ │ 独立仓库  │
                  │ 独立部署   │ │ 独立部署  │ │ 独立部署  │
                  └──────────┘ └──────────┘ └──────────┘

每个子应用:独立的仓库、独立的技术栈、独立的部署流水线、独立的团队。基座只负责在运行时把它们拼起来。

微服务就是把这套思路搬到后端。 后端的"巨石应用"叫单体(Monolith),拆出来的"子应用"叫微服务(Microservice)。下面这张对照表先建立直觉:

微前端(你已经会的) 微服务(后端版)
巨石前端应用 单体应用 Monolith
qiankun / Module Federation Spring Cloud 微服务框架
子应用(独立 SPA) 微服务(独立 Spring Boot 进程)
主应用基座(路由分发) API 网关 Gateway
子应用注册到基座 服务注册到 Eureka
loadMicroApp 按需加载子应用 Feign 远程调用其他服务
子应用各自 npm run build 上线 每个服务各自打 jar 包独立部署
共享登录态 / 公共依赖 公共组件库 demo-common

记住一句话就够了:微服务 = 微前端的后端版。 你理解微前端为什么诞生,就理解了微服务为什么诞生——它们要治的是同一种病。


二、单体 vs 微服务:到底差在哪

2.1 单体应用长什么样

demo 里就有活的单体——demo-core(PHP / ThinkPHP 3.x),示例平台最核心、最古老的后端。它一个应用里塞了 30 多个业务模块(见知识地图):

┌────────────────────────────────────────────────┐
│                  demo-core(单体)                  │
│                                                  │
│  Order   Truck   Finance  Driver   Salary        │
│ (运单) (车辆) (财务) (司机) (工资)       │
│                                                  │
│  Settle  Insurance Warehouse Shipper  Basic ...  │
│ (结算) (保险)  (仓储)  (货主) (基础)     │
│                                                  │
│         全部跑在同一个进程、同一份代码里           │
│         共用一个数据库、一起编译、一起部署          │
└────────────────────────────────────────────────┘

单体的好处(别一上来就觉得单体是坏东西):

  • 改起来快,一个仓库全搞定,跨模块调用就是普通函数调用,不用走网络
  • 部署简单,一个包丢上去就行
  • 没有分布式那一堆破事(网络超时、数据一致性、链路追踪)

所以 demo 最核心的业务至今还放在 demo-core 这个单体里,因为它够稳、够快、改动频繁。这是个非常务实的选择,不是技术债。

单体的痛点(和巨石前端一模一样):

  • 代码越堆越大,新人看一年都摸不清全貌
  • 任何一个小改动都要把整个应用重新部署,发版风险大
  • 想给"导出报表"这种吃 CPU 的功能单独加机器?做不到,只能整个应用一起扩容
  • 技术栈被锁死,整个 demo-core 想从 PHP 换成别的,等于重写

2.2 微服务长什么样

demo 的 Java 服务群就是微服务(见知识地图的项目总览):

                    ┌──────────────────────────┐
                    │  demo-openresty-gateway    │  ← API 网关(基座)
                    │     统一入口 / 鉴权 / 路由  │
                    └────────────┬──────────────┘
                                 │
        ┌──────────┬─────────────┼─────────────┬──────────┐
        ▼          ▼             ▼             ▼          ▼
  ┌─────────┐┌──────────┐┌────────────┐┌──────────┐┌──────────┐
  │demo-basic││demo-billing││demo-asset││demo-pay  ││demo-export│
  │基础信息  ││  财务     ││  车务中心    ││  支付    ││  导出     │
  │独立jar  ││ 独立jar   ││  独立jar    ││ 独立jar ││ 独立jar  │
  │独立部署  ││ 独立部署  ││  独立部署    ││ 独立部署 ││ 独立部署  │
  └─────────┘└──────────┘└────────────┘└──────────┘└──────────┘
        每个都是独立的 Spring Boot 进程,可以单独发版、单独扩容

一眼能看出的差别:

维度 单体(demo-core) 微服务(demo-basic 等)
部署单元 1 个大应用 N 个独立 jar 进程
改一处的影响 整个应用重新部署 只重启那一个服务
扩容粒度 整个应用一起扩 哪个服务忙就扩哪个
技术栈 全锁定(都是 PHP) 每个服务可不同(见下)
服务间调用 直接函数调用 走网络(HTTP/Feign)
数据库 通常共用一个 倾向各管各的库
故障范围 一处崩可能拖垮全部 单个服务崩,其他还能跑

注意 demo 的技术栈混搭得很自然:demo-core 是 PHP、demo-notify 是 Go、那一堆 demo-basic / demo-billing 是 Java。这正是微服务的红利——每个团队/每个服务用最顺手的技术,互不干扰。就像微前端里订单子应用用 React、财务子应用用 Vue2,谁也不强迫谁统一。


三、demo 为什么拆这么多服务?拆分原则

打开知识地图你会发现 demo 的 Java 服务命名极有规律,这些名字本身就暴露了拆分原则——按业务能力(Business Capability)划分,一个服务管一摊业务:

服务 管的事 类比微前端的哪个子应用
demo-basic 公共基础信息(用户、车辆、组织、司机) 公共数据子应用
demo-billing 财务 财务子应用
demo-payroll 司机工资 工资子应用
demo-asset 车务中心(车辆业务) 车辆子应用
demo-pay 支付 支付子应用
demo-pricing 计价 计价子应用
demo-export 导出 导出子应用
demo-intransit 在途 在途子应用
demo-messagechannel 消息中心 消息子应用

拆分时大致遵循这几条原则,每条都能在前端找到对应感觉:

  1. 按业务边界拆,不按技术分层拆 不会拆成"Controller 服务 / Service 服务 / DAO 服务"(那是灾难),而是拆成"财务服务 / 支付服务"。 前端类比:微前端按"订单/财务/车辆"拆子应用,不会按"组件层/状态层"拆。

  2. 高内聚低耦合 一件事尽量在一个服务内闭环,跨服务调用越少越好。财务相关的逻辑都收在 demo-billing 里,别散落到各处。 前端类比:一个 feature 的组件、store、API 都放在同一个子应用 / 同一个目录里。

  3. 按变化频率/扩容需求拆 导出报表(demo-export)很吃资源、调用又不频繁,单独拆出来就能单独加机器,不影响主链路。 前端类比:一个超重的可视化大屏单独拆成子应用,免得拖慢主应用首屏。

  4. 公共能力下沉成共享库,而不是做成服务 demo-common(公共组件)、demo-parent(Maven 父 POM)、demo-parent-enums(公共枚举)这几个不独立部署,是被各服务以依赖方式引入的库。 前端类比:公共的 utils / UI 组件库发成 npm 包,各子应用 npm install,而不是为它单独起一个子应用。

一句话原则:能让一个团队独立开发、独立部署、独立扩容、出了故障也不连累别人的那条业务线,就该是一个服务。


四、服务之间怎么协作?(重点)

微前端里子应用之间要通信,靠的是基座提供的 props、全局事件总线、或者 URL。微服务之间通信主要靠"网络调用"——一个服务通过 HTTP 去请求另一个服务的接口。

demo 的服务间协作用的是 Spring Cloud(Eureka + Feign),再加上一个独立的对外网关,三个关键角色:

   ① 服务注册发现(Eureka)          ② 远程调用(Feign)         ③ 统一入口(Gateway)
   ┌────────────────────┐
   │      Eureka         │   "我是 demo-basic,
   │   服务注册中心       │◄── 我在 10.0.0.5:8080,记一下"
   │  谁在哪 都登记在这    │
   └─────────┬──────────┘
             │ "demo-basic 在哪?" "在 10.0.0.5:8080"
             ▼
   ┌──────────────┐   Feign 发起 HTTP 请求    ┌──────────────┐
   │ demo-asset│ ───────────────────────► │  demo-basic    │
   │  车务中心      │ ◄─────────────────────── │  基础信息      │
   └──────────────┘      返回 JSON 数据         └──────────────┘

4.1 服务注册与发现:Eureka

问题:demo-asset 想调 demo-basic 的接口,但 demo-basic 部署了好几台机器,IP 还会变(容器随时重启换 IP),怎么找到它?

答案:每个服务启动时都向 Eureka(注册中心) 报到:"我是 demo-basic,我在这个 IP:端口"。要调用方就问 Eureka:"demo-basic 在哪?" Eureka 把可用地址给它。

前端类比:qiankun 里子应用要先 registerMicroApps 注册到基座,基座才知道它的入口地址。Eureka 就是后端的"注册表"。所以你调用别的服务时只写服务名 demo-basic,从来不写死 IP。

4.2 远程调用:Feign(最常用,要重点掌握)

Feign 是 demo 服务间调用的主力。它最妙的地方是:让"调用另一个服务的远程接口"写起来跟"调用本地的一个接口方法"几乎一样

看一段 demo 匿名化示例代码——demo-asset 里声明了一个客户端去调 demo-basic 的用户接口:

文件:demo-asset/src/main/java/com/example/platform/vehicle/biz/client/basic/BasicUserClient.java

// @FeignClient 声明这是一个远程调用客户端
// name = "demo-basic" —— 目标服务名(不是 IP!Feign 会去 Eureka 查实际地址)
// contextId —— 同一个服务有多个 client 时用来区分,避免 Bean 冲突
// configuration —— 指定编解码器,这里处理蛇形命名(snake_case)字段转换
@FeignClient(contextId = "basicUserClient", name = "demo-basic",
        configuration = SnakeCaseEncoderAndDecoder.class)
public interface BasicUserClient {

    /**
     * 根据 id 查询用户
     * 注意:这里只是声明"远程接口长什么样",没有任何实现代码
     * Feign 会在运行时自动生成实现:把这个方法调用变成一次 HTTP GET 请求
     *
     * @param id 用户 id,作为 query 参数拼到 URL 上
     * @return 统一响应包装 DemoCommonRespDTO,里面装着用户数据
     */
    @GetMapping("/api/user/getById")
    DemoCommonRespDTO<BasicUserRespDTO> queryById(@RequestParam("id") Integer id);

    /**
     * 批量根据 ids 查询用户
     * 用 POST 是因为 id 集合可能很多,放 body 里比塞 URL 更合适
     *
     * @param ids 用户 id 集合
     * @return 用户列表
     */
    @PostMapping("/api/user/listByIds")
    DemoCommonRespDTO<List<BasicUserRespDTO>> queryById(Collection<Integer> ids);
}

注意:这是一个 interface,没有任何方法体。你只是"声明"了远程接口的样子(路径、参数、返回类型),Feign 在运行时帮你生成真正发 HTTP 请求的实现。

调用方用起来就像调本地方法一样自然:

// 注入 Feign 客户端,跟注入普通 Service 一模一样(@Autowired 见第 5 课)
@Autowired
private BasicUserClient basicUserClient;

public void demo() {
    // 看起来是普通方法调用,背后其实是:
    // Feign → 问 Eureka 要 demo-basic 地址 → 发 HTTP GET /api/user/getById?id=123 → 解析返回 JSON
    DemoCommonRespDTO<BasicUserRespDTO> resp = basicUserClient.queryById(123);
    BasicUserRespDTO user = resp.getData();  // 拿到对面服务返回的用户数据
}

前端类比:这简直就是后端版的 类型化 API client。你在前端是不是经常封装一个 userApi.getById(123),底层用 axios 发请求?Feign 就是这个套路,只不过它连"封装"这步都帮你省了——你写个带注解的 interface,它自动生成那个 axios 风格的实现。@GetMapping/@RequestParam 这些注解见第 8 课。

4.3 统一入口:API 网关

前端调后端时,难道要记住"用户接口找 demo-basic、财务接口找 demo-billing"几十个地址吗?当然不。所有请求先打到 demo-openresty-gateway(API 网关),由它统一鉴权、再按路由转发到对应的内部服务。

注意一个细节:demo 的网关不是 Spring Cloud Gateway,而是基于 OpenResty(Nginx + Lua) 自己搭的(看项目里全是 nginx.conf.lua 脚本,没有 pom.xml)。也就是说"服务间用 Eureka+Feign 互相调"和"对外用 OpenResty 网关统一入口"是两套独立的东西——前者是 Spring Cloud 体系内的,后者是独立的反向代理层。这在国内中大型团队里很常见:内部服务发现走 Spring Cloud,最外层入口用更成熟、性能更好的 Nginx 系网关扛流量。

前端类比:网关 = qiankun 基座的路由分发。前端只认一个域名(一个基座入口),基座内部决定把当前路由交给哪个子应用渲染。


五、微服务的优缺点(务实地看)

别迷信微服务,它是有代价的。下面这张表帮你建立"什么时候该拆、什么时候别拆"的判断力:

✅ 优点 ⚠️ 代价
独立部署:改一个服务不影响别人,发版风险小 运维复杂:从管 1 个应用变成管 N 个,要监控、要日志聚合
独立扩容:哪个服务忙单独加机器(如 demo-export) 分布式难题:网络会超时、会失败,要做重试、降级、熔断
技术栈自由:PHP/Go/Java 混用(见 demo) 数据一致性难:跨服务的事务没法用一个数据库事务搞定
故障隔离:单个服务崩,其他还能跑 调试变难:一个请求跨好几个服务,排查要靠链路追踪
团队解耦:每个团队管自己的服务,并行开发 调用变慢:本地函数调用变成了网络请求,有延迟

和微前端的取舍是一样的:项目小、团队小,别拆,单体最香(这就是 demo-core 至今是单体的原因);项目大、团队多、有人想独立发版独立扩容了,再拆。先有单体的痛,再上微服务的药。 一上来就拆微服务,等于给个人博客上 qiankun——纯找罪受。


六、本课小结

  • 微服务 = 微前端的后端版:单体 ↔ 巨石前端,微服务 ↔ 子应用,网关 ↔ qiankun 基座,Feign ↔ 类型化 API client。理解微前端就理解了微服务。
  • 单体不是坏东西:demo 最核心的业务至今放在单体 demo-core 里,因为够稳够快。微服务是用来治"大团队 + 高频发版 + 差异化扩容"这种病的,没那个病别乱吃药。
  • demo 的拆分原则:按业务能力拆(demo-basic/demo-billing/demo-pay…),高内聚低耦合,按变化频率和扩容需求拆,公共能力下沉成共享库(demo-common,不独立部署)。
  • 服务间协作:Eureka 做服务注册发现(只认服务名不写死 IP)、Feign 做远程调用(声明式 interface,写起来像调本地方法),这两个是 Spring Cloud 体系内的;最外层另有独立的 OpenResty 网关(demo-openresty-gateway)做统一入口和鉴权,它不属于 Spring Cloud。
  • 匿名化示例代码BasicUserClient@FeignClient(name = "demo-basic") 声明远程接口,demo-asset 靠它跨服务调 demo-basic 的用户数据。
  • 微服务有代价:运维复杂、分布式难题、数据一致性、调试变难、调用变慢——拆之前先掂量清楚。

下一课预告:第 22 课《Spring Cloud 入门:Eureka 与 Feign 实战》。这一课我们只是建立了"为什么拆、怎么协作"的全局观,下一课会手把手带你看 demo 是怎么用 Spring Cloud 把一个服务注册进 Eureka、再用 Feign 把另一个服务的接口调起来的,让你能真正读懂 demo-asset 那一堆 client 目录下的代码。

七、总结

  • 先看个你熟悉的东西:微前端:基座只负责在运行时把它们拼起来。
  • 单体 vs 微服务:到底差在哪:改起来快,一个仓库全搞定,跨模块调用就是普通函数调用,不用走网络
  • demo 为什么拆这么多服务?拆分原则:按业务边界拆,不按技术分层拆 -> 高内聚低耦合 -> 按变化频率/扩容需求拆 -> 公共能力下沉成共享库,而不是做成服务
  • 服务之间怎么协作?(重点):微前端里子应用之间要通信,靠的是基座提供的 props、全局事件总线、或者 URL。
  • 微服务的优缺点(务实地看):别迷信微服务,它是有代价的。
  • 本课小结:单体不是坏东西:demo 最核心的业务至今放在单体 demo-core 里,因为够稳够快。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“微服务是什么”中,需要同时满足“先看个你熟悉的东西:微前端”与“单体应用长什么样”。给定正文约束“基座只负责在运行时把它们拼起来。”,哪些判断保持了原有处理机制?多选
2“微服务是什么”出现偏差:“在“微服务是什么 / 微服务长什么样”中,即使不满足“demo 的 Java 服务群就是微服务(见知识地图的项目总览)”,结果与副作用仍会保持不变。”已成为实际行为。围绕“微服务长什么样”与“demo 为什么拆这么多服务?拆分原则”,哪些判断能定位被改变的职责或边界?多选
3评审“微服务是什么”方案时,验收条件包含“微服务之间通信主要靠"网络调用"——一个服务通过 HTTP 去请求另一个服务的接口。”。关于“服务之间怎么协作?(重点)”与“服务注册与发现:Eureka”的哪些决策符合正文机制?多选