主题
从单体到微服务:演进与拆分原则
本文是微服务架构系统学习系列的 L1 入门篇。前置:建议先读分布式系列 26. from-monolith-to-distributed-cap-base。 学完可以配合面试题食用:01-soa-vs-microservice、16-monolith-to-microservices-seven-steps、20-microservice-problem-450-vs-6-modules
单体不丢人:先别焦虑
2025 年的技术社群,一说"还在用单体"就矮半截。但事实是:单体是中小团队的最优解,不是技术债。
一个 10 人团队,一个 Spring Boot 单体服务,每天 deploy 10 次,迭代速度比拆成 20 个微服务快得多。为什么?因为微服务每个部署都要走 CI/CD、配置中心、服务发现、可观测性,这一套基础设施选型+搭建至少 2-3 周,而单体改一行代码就 mvn package && scp 上去了。
有个反直觉的规律:团队规模 < 10 人时,单体比微服务快;10-15 人时,模块化单体够用;超过 15 人,才轮到微服务上场。
// 一个简单的单体支付模块,一行代码搞定事务
@Transactional
public void pay(Order order, Account account) {
orderRepo.save(order);
accountRepo.deduct(account, order.getAmount());
}换成微服务,这段代码变成:订单服务调账户服务调用余额服务,每步都得处理网络超时、幂等、分布式事务。成本翻 5 倍,产出是零。
什么时候该拆:三个信号
拆服务不是未雨绸缪,是被痛点逼的。出现以下三个信号之一,再考虑拆:
信号一:部署互相阻塞。 订单组改了数据库表结构,通知组得等半天才能上线。两个功能的发版周期从 1 天变成 1 周。这时候"一起部署"不再是优势,而是瓶颈。
信号二:故障爆炸半径太大。 一个内存泄漏的 OOM 异常,把整个应用拖垮,所有接口 502。用户模块挂了,连带支付、搜索、通知全停。单体里没有故障隔离,一个 bug 等于全站挂。
信号三:团队规模触到康威定律临界点。 康威定律说:"设计系统的架构受制于产生这些设计的组织的沟通结构。" 翻译成人话:两个团队协作维护同一个代码库,接口文档和代码合并就是每天的战场。 15 人以上的团队还在一个单体仓库里开发,每个 PR 的冲突概率和 code review 的等待时间都会指数级增长。
拆分原则:按业务能力,不是按技术层
很多团队第一步就错了:按技术层拆——前端一个服务、后端一个服务、数据库一个服务。这不叫微服务,这叫把单体垂直切了三刀。
正确做法是按业务能力(DDD 的限界上下文)拆分:
yaml
# 按技术层拆(错误)
services:
- controller-service # 所有 Controller 放一起
- service-service # 所有 Service 放一起
- dao-service # 所有 DAO 放一起
# 按业务能力拆(正确)
services:
- order-service # 订单上下文:订单 + 订单项 + 支付记录
- user-service # 用户上下文:用户 + 地址 + 积分
- payment-service # 支付上下文:交易 + 退款 + 对账按业务能力拆有几个硬约束:
- 数据库必须跟着拆。每个服务拥有自己的数据库,其他服务只能通过 API 访问。两个服务读写同一张表,等于没拆。
- 服务间只通过接口通信,不共享 Domain 对象。order-service 的 Order 对象和 payment-service 的 OrderDTO 是两个东西,不要用同一个类。
- 每个服务自治:自己能独立部署、独立升版、独立扩缩容。
拆分路径:绞杀者模式是正解
不要搞"big bang 重写"——那个死在 archive 文件夹里的项目叫 v2。正确路径是绞杀者模式(Strangler Fig Pattern):
┌─────────────── 单体应用 ───────────────┐
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ 订单模块 │ │ 用户模块 │ │ 支付模块 │ │
│ └────┬────┘ └────┬────┘ └─────┬────┘ │
└───────┼───────────┼─────────────┼──────┘
│ │ │
│ 第一步 │ │
│ 模块化 │ │
▼ ▼ ▼
┌─────────────── 单体应用 ───────────────┐
│ ┌──────────────────────────────────────┐│
│ │ 订单模块 (清晰包边界+接口) ││
│ │ 用户模块 (清晰包边界+接口) ││
│ │ 支付模块 (清晰包边界+接口) ││
│ └──────────────────────────────────────┘│
└──────────────────────────────────────────┘
│ │ │
│ 第二步 │ │
│ 抽独立服务 │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌──────────┐
│ 订单服务 │ │ 用户服务 │ │ 支付服务 │
│ (独立进程) │ │ (独立进程) │ │ (独立进程) │
└─────────┘ └─────────┘ └──────────┘第一步:模块化单体。 不拆进程,先拆代码结构。把单体应用内的包结构按业务边界重新组织,每个模块只通过接口暴露能力,内部实现完全隔离。这一步不需要任何基础设施投入,成本最低。
第二步:逐个抽离。 选一个边界最清晰、依赖最少的模块(通常是用户模块),先抽成独立服务。老单体通过 RPC 调用新服务,新服务通过 API 查询老单体——中间状态可以共存几个月。这就是绞杀者模式的核心:不一次推倒,一片一片替换。
下面是一个模块化单体到微服务的目录结构对比:
# 单体(混乱版)
src/main/java/com/example/
├── controller/ # 所有 Controller
├── service/ # 所有 Service
├── dao/ # 所有 DAO
├── model/ # 所有 Entity
└── Application.java
# 模块化单体(清晰版)
src/main/java/com/example/
├── order/
│ ├── OrderController.java
│ ├── OrderService.java # interface
│ ├── OrderServiceImpl.java # package-private
│ └── OrderRepository.java
├── user/
│ ├── UserController.java
│ ├── UserService.java
│ ├── UserServiceImpl.java
│ └── UserRepository.java
└── payment/
├── PaymentController.java
├── PaymentService.java
├── PaymentServiceImpl.java
└── PaymentRepository.java微服务的税单:算完账再拆
微服务不是免费的升级。拆之前,把这些"税"算清楚:
| 税项 | 单体 | 微服务 |
|---|---|---|
| 网络调用 | 方法调用,≈0ms | RPC,≈1-10ms,还有超时/重试 |
| 事务 | @Transactional 搞定 | 分布式事务(TCC/Saga),至少 10 倍复杂度 |
| 可观测性 | 一个日志文件 | 链路追踪 + 指标 + 结构化日志,一套基础设施 |
| 部署 | scp 一个 jar | CI/CD + 容器编排 + 配置中心 + 服务发现 |
| 测试 | 本地跑完整 | 契约测试 + 集成测试 + 端到端测试 |
一个数量级参考: 拆成 10 个微服务,运维成本大约是单体的 5-8 倍。如果业务复杂度不够,这些成本就是纯浪费。
常见误区与小结
常见误区:
- "微服务 == 性能好":错了。微服务多了网络延迟,性能通常比单体差。微服务解决的是组织效率问题,不是性能。
- "先拆了再说,后面再重构":拆之前没有清晰的业务边界,拆完就是 10 个更难维护的单体,每个都耦合。
- "每个服务都用不同语言":多语言微服务增加了运维成本和团队协作成本。15 人团队推荐统一语言,除非有硬性理由(如性能敏感模块用 Go)。
- "微服务解决了所有问题":微服务解决的是部署耦合和团队协作问题,但引入了分布式事务、网络不可靠、运维复杂度。没有银弹。
- "拆完就完了":微服务架构需要持续治理——服务版本管理、接口兼容性、依赖关系图,这些是长期维护工作。
小结:
单体是起点,模块化单体是过渡,微服务是痛到不得不做的选择。本文讲清楚了"什么时候拆"和"怎么拆"——下一个核心问题是拆完之后,服务之间怎么互相找到(注册与发现)和怎么管理配置,即 28. registry-config-center。
参考
参考:Martin Fowler, "Microservices" (2014) — 微服务定义原文;Sam Newman, "Building Microservices" 第 1-3 章;Eric Evans, "Domain-Driven Design" 限界上下文章节。