知识点思维导图
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 | 消息中心 | 消息子应用 |
拆分时大致遵循这几条原则,每条都能在前端找到对应感觉:
-
按业务边界拆,不按技术分层拆 不会拆成"Controller 服务 / Service 服务 / DAO 服务"(那是灾难),而是拆成"财务服务 / 支付服务"。 前端类比:微前端按"订单/财务/车辆"拆子应用,不会按"组件层/状态层"拆。
-
高内聚低耦合 一件事尽量在一个服务内闭环,跨服务调用越少越好。财务相关的逻辑都收在 demo-billing 里,别散落到各处。 前端类比:一个 feature 的组件、store、API 都放在同一个子应用 / 同一个目录里。
-
按变化频率/扩容需求拆 导出报表(demo-export)很吃资源、调用又不频繁,单独拆出来就能单独加机器,不影响主链路。 前端类比:一个超重的可视化大屏单独拆成子应用,免得拖慢主应用首屏。
-
公共能力下沉成共享库,而不是做成服务 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 里,因为够稳够快。
学完自测
选择所有正确答案;提交后逐项核对判断依据。