第 5 篇讲过”单体还是微服务怎么选”,当时我们的答案是单体起步。三年后,主系统长到了 20 多人共同开发、每月 40+ 次发布,单体开始顶不住——这篇记录我们用一年时间把它拆成微服务的完整过程:为什么拆、怎么拆、代价付在哪、哪些反模式差点踩进去。
拆分的触发信号
拆分不是架构审美问题,是具体的工程约束。我们当时的四个信号:
| 信号 | 我们的实测 | 阈值参考 |
|---|---|---|
| 部署频率 | 每月 40+ 次,全量回归 2 小时 | 发布排队开始常态化 |
| 团队规模 | 22 人改同一个仓库 | 经验值:10-15 人/单体仓库是舒适上限 |
| 故障隔离 | 报表慢查询拖垮在线下单 | 单点故障影响面 > 一个业务域 |
| 构建时长 | CI 全量 50 分钟 | 修复一个字段的反馈周期不可接受 |
只中一条不必拆,中了三条还硬扛,成本会以故障和人员流失的形式追讨。
JHipster 微服务拓扑回顾
第 5 篇的拓扑图,拆分前再复习一遍各角色职责:
flowchart LR
U["浏览器/App"] --> GW["JHipster Gateway<br/>路由聚合 / OAuth2-JWT 下发"]
GW -->|服务发现| REG["JHipster Registry<br/>Eureka + Config Server"]
subgraph SVCS["业务服务(独立库)"]
ORD["order-service"]
INV["inventory-service"]
USR["user-service"]
end
GW --> ORD & INV & USR
ORD -.->|同步 API| INV
ORD -.->|事件| K["Kafka"]
要点:Gateway 只做路由、限流与鉴权传递,不写业务;Registry 同时承担服务发现与集中配置(第 21 篇的 Config Server 落点);服务间同步走 REST/Feign,异步走 Kafka(第 12 篇)。
拆分四步法
总原则一句话:先把单体内部拆干净,再拆进程边界。顺序反了就是分布式单体。
第一步:单体内模块化
不引入任何分布式设施,只做包边界硬化(正好用上第 23 篇的模块化形态二):
# 拆分前的单体:包按技术分层
com/mycompany/myapp/{domain,service,web/rest} # 订单用户库存混在一起
# 模块化后:Maven module 按业务域
myapp/
├── myapp-order/ # 订单域:自己的 domain/service/api(接口)
├── myapp-inventory/
├── myapp-user/
└── myapp-app/ # 组装壳:依赖三个模块,跑成单体
这一步的验收标准:模块间依赖只通过 api 接口,用 ArchUnit 固化成测试。数据库同步处理——三个域仍共库,但表按前缀划分(ord_/inv_/usr_),跨域直接查表被 CI 禁止。我们在单体内跑稳了三个月,期间修掉的隐式耦合(跨包 private 方法调用、JOIN 跨域表)比后面所有步骤加起来都多,也都可以在单进程里低成本调试。
第二步:按业务域抽服务
从最独立、发布最频繁的域开始。我们是订单域先行。JHipster 这一步的体验很顺:新起微服务工程,JDL 里重建实体(或从 .jhipster/*.json 迁移),网关重新注册:
mkdir order-service && cd order-service
jhipster --application-type microservice --jwt --database-type postgresql
# 在网关侧导入微服务实体路由
jhipster entity Order --microservice-name order-service
迁移代码就是把 myapp-order module 平移进新服务,删掉对单体的 api 依赖。抽出一个上线一个,单体里对应模块冻结只读。六个月内三个服务全部出舱:
| 服务 | 实体数 | 库 | 上线耗时 |
|---|---|---|---|
| order-service | 14 | 独立 PostgreSQL | 6 周(含数据迁移演练) |
| inventory-service | 9 | 独立 PostgreSQL | 3 周 |
| user-service | 6 | 独立库 | 2 周 |
第三步:共享数据拆分(最难的一段)
模块化时按前缀分的表,现在按域切库。三条铁律:
没有共享库,只有共享 API。 订单服务要库存余量,走 inventory-service 的 REST 接口,不连它的库。JHipster 生成代码里加 Feign client:
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/api/inventory/available")
List<AvailabilityDTO> available(@RequestParam List<String> skus);
}
读走 API,写用事件。 跨域的数据复制用事件同步:库存变更发 InventoryChangedEvent 到 Kafka,订单服务消费后更新本地冗余表。为避免”发消息和落库”的双写不一致,用了outbox 模式——业务事务里先写 outbox 表,relay 进程再搬运到 Kafka:
-- 与业务同事务
INSERT INTO ord_order (...) VALUES (...);
INSERT INTO ord_outbox (topic, payload) VALUES ('order-events', '{"orderId":...}');
-- 独立 relay 进程扫 outbox 发 Kafka,发成功标记,失败重试
跨库事务不做 2PC,用 Saga 补偿。 下单扣库存:订单创建(pending)→ 扣库存 → 确认;扣失败则订单走补偿取消。 saga 状态机就一张 ord_saga_state 表加一个驱动器类,没有引入重型框架。
第四步:网关路由与鉴权下沉
拆分后认证统一收敛:JWT 由网关下发(或对接公司 OAuth2/OIDC),各微服务只做令牌校验与角色判断,JHipster 生成的 SecurityConfiguration(9.x 下含 EnableWebSocketSecurity 的场景也在网关终结握手)天然支持这个模式。服务间调用透传 Authorization header。网关路由即配置:
# gateway 的 application.yml
spring:
cloud:
gateway:
routes:
- id: order
uri: lb://order-service
predicates: [ Path=/services/order/api/** ]
- id: inventory
uri: lb://inventory-service
predicates: [ Path=/services/inventory/api/** ]
拆分的代价表
拆完不是终点,是新的成本结构开始:
| 维度 | 单体时 | 拆分后 | 我们的对冲 |
|---|---|---|---|
| 分布式事务 | 单库 ACID | Saga/最终一致,心智负担高 | outbox + 补偿任务 + 对账日报 |
| 联调 | 本地起一个进程 | 起 5 个进程 + Registry | docker compose 一键拓扑 + 契约测试 |
| 排障 | 一个日志文件 | 跨服务链路 | 集中日志 + traceId 贯穿(第 20 篇) |
| 运维 | 1 个部署单元 | 4 个 + Registry + Gateway | K8s + GitOps(第 19 篇) |
| 数据一致性 | 立即可见 | 秒级延迟可接受化 | 业务上明确”读己之写”的例外通道 |
| 发布 | 全量 | 独立发布,频率 4 倍 | 收益本体 |
诚实地说:总成本没有下降,只是成本结构从”发布摩擦”换成了”复杂度税”。划算与否取决于发布频率收益是否大于税。我们是 40 次/月变 160 次/月,换来了。
反模式警示:分布式单体
最危险的结果不是拆失败,而是拆出分布式单体:服务边界按技术切而不是按业务切、服务间同步调用链长到一环挂全环、共享库导致谁也不敢先升级。症状自检:
- 一次需求平均要改 3 个以上服务才能上线——边界切错了;
- 发布仍要”按顺序停全部服务”——你在用微服务的成本买单体的行为;
- 数据库视图/共享表跨服务直接 JOIN——数据所有权没拆干净。
这类问题的完整论述推荐站内文章《走出微服务误区》。我们拆分第一版就踩了”按技术分层切服务”的坑(一个 service 层服务、一个 repository 服务),三个月后推倒按业务域重切——这也是第一步必须先做单体内模块化的原因:边界错了,在单体内重划只要一周。
小结
- 拆分看信号不看审美:部署频率、团队规模、故障隔离、构建时长,三条以上再动手。
- 四步走:单体内模块化(验收是 ArchUnit 硬边界)→ 按域抽服务(独立独立再独立)→ 数据拆分(API + outbox 事件 + Saga)→ 网关路由与鉴权下沉。
- JHipster 的微服务拓扑(Gateway/Registry/服务)让第 0 天就有正规拓扑,拆分工作聚焦在业务边界本身。
- 最大的反模式是分布式单体,先模块化后拆分是最好的疫苗。
系列导航
- 上一篇:子生成器二次开发与蓝图机制
- 下一篇:性能调优实战
- 相关阅读:拆分前的架构决策见第 5 篇:微服务 vs 单体;服务间异步通信基础见第 12 篇 Kafka;配套的可观测性见第 20 篇与第 19 篇 K8s