微服务拆分实战——如何识别拆分边界,避免"分布式单体"陷阱
微服务拆分实战——如何识别拆分边界,避免"分布式单体"陷阱
适读人群:正在进行微服务拆分或架构重构的后端开发者和架构师 | 阅读时长:约17分钟 | 核心价值:掌握微服务边界识别方法论,避免拆了微服务却做出"分布式单体"
拆了一年,回头发现啥都没变
2021 年底,我认识了一个在某 B2B 平台做架构的朋友老方。他们花了整整一年时间把原来的单体应用"改造成微服务"——拆成了 30 多个服务,全套 Spring Cloud,Docker + K8s 部署,看起来很现代。
但我问他:你们拆了之后,部署是分开部署了吗?
他说:"不是,我们 30 多个服务必须同时发版,因为有很多接口是相互依赖的,改一个接口,要同时改好几个服务的代码。"
那你们线上故障隔离了吗?
"没有,一个服务出问题,很快会蔓延到相关联的服务,排查起来比以前更难。"
我当时心里已经有了答案:他们拆的不是微服务,而是分布式单体——形式上是多个独立部署的服务,但逻辑上依然是一个整体,服务之间耦合过深,失去了微服务的核心价值。
这个问题比单体更糟糕:你同时拥有了单体的耦合,和分布式系统的复杂性。
微服务拆分的本质:业务能力 + 数据自治
微服务不是"把代码切成小块",而是要实现:
- 业务能力独立:每个服务负责一个清晰的业务能力,有明确的边界
- 数据自治:每个服务有自己的数据库,不共享数据库
- 独立部署:服务可以独立部署,不需要其他服务同时发版
- 故障隔离:一个服务的故障不会直接导致其他服务不可用
这四条,缺一条就不是真正的微服务。
边界识别方法一:领域驱动设计(DDD)
DDD 是目前识别微服务边界最系统的方法。核心概念是限界上下文(Bounded Context)——在一个业务领域内,用统一语言(Ubiquitous Language)描述的一个边界清晰的领域模型。
事件风暴工作坊
识别限界上下文最有效的方法是事件风暴:
- 找 Domain Event(领域事件):把业务中发生的重要事情用橙色便签写出来,动词过去式("订单已创建"、"支付已成功"、"库存已扣减")
- 找 Command(命令):触发事件的操作("创建订单"、"发起支付")
- 找 Aggregate(聚合):处理命令、产生事件的业务对象("订单"、"支付"、"库存")
- 识别 Bounded Context:把相关的聚合和事件归组,每组就是一个候选服务
电商系统的典型 Bounded Context:
订单上下文(Order Context)
├── 聚合:Order(订单)
├── 聚合:OrderItem(订单项)
└── 领域事件:OrderCreated, OrderCancelled, OrderCompleted
支付上下文(Payment Context)
├── 聚合:Payment(支付)
├── 聚合:Refund(退款)
└── 领域事件:PaymentSucceeded, PaymentFailed, RefundCompleted
库存上下文(Inventory Context)
├── 聚合:Stock(库存)
├── 聚合:StockReservation(库存预占)
└── 领域事件:StockDeducted, StockReleased, StockInsufficient
用户上下文(User Context)
├── 聚合:User(用户)
├── 聚合:Address(地址)
└── 领域事件:UserRegistered, AddressAdded用代码体现边界
识别出边界后,在代码层面体现这个边界:
src/
├── order/
│ ├── domain/
│ │ ├── Order.java # 聚合根
│ │ ├── OrderItem.java # 实体
│ │ ├── OrderStatus.java # 值对象
│ │ └── OrderCreatedEvent.java # 领域事件
│ ├── application/
│ │ ├── OrderApplicationService.java
│ │ └── CreateOrderCommand.java
│ ├── infrastructure/
│ │ ├── OrderRepositoryImpl.java
│ │ └── OrderMapper.java
│ └── interfaces/
│ └── OrderController.java关键原则: Order 聚合里绝对不能直接引用 Payment 或 Inventory 的实体。跨上下文的通信通过领域事件(消息队列)或防腐层(Anti-Corruption Layer)进行。
边界识别方法二:识别"上下文映射"
定义了各个上下文之后,还要识别它们之间的关系:
// 错误方式:Order 直接引用 Payment(破坏边界)
@Entity
public class Order {
@ManyToOne
private Payment payment; // ❌ 不同上下文不应直接引用
}
// 正确方式:Order 只保存跨上下文的 ID 引用
@Entity
public class Order {
private String paymentId; // ✅ 只存 ID,不存对象引用
// 如果需要支付信息,通过防腐层查询支付服务
public PaymentInfo getPaymentInfo(PaymentServiceAdapter adapter) {
return adapter.getPaymentInfo(this.paymentId);
}
}
// 防腐层:隔离外部上下文的模型变化
@Component
public class PaymentServiceAdapter {
@Autowired
private PaymentServiceClient paymentClient;
public PaymentInfo getPaymentInfo(String paymentId) {
// 调用支付服务,将支付服务的 DTO 转换为本上下文需要的模型
PaymentDTO dto = paymentClient.getPayment(paymentId);
return convertToPaymentInfo(dto);
}
}判断是否拆分过细的标准
拆分不是越细越好。以下情况说明你可能拆太细了:
1. 一个业务功能需要调用超过 5 个服务
如果完成一个用户操作,需要调用 6-8 个微服务,这说明拆分粒度过细,业务流程无法在一个服务内完成。
2. 两个服务总是同时修改
如果每次需求都要同时改 Service A 和 Service B,说明它们应该是一个服务。
3. 两个服务共享同一个数据库表
这是最明显的信号,说明拆分没有做到数据自治。
4. 服务粒度小于一个团队
康威定律:系统架构反映组织架构。一个服务应该由一个完整的小团队(3-8人)负责,如果一个服务只有一行代码或一个接口,拆得太细了。
实战:识别拆分边界的代码评审标准
// 代码评审时检查清单
// ❌ 警告:直接引用其他服务的实体
public class OrderService {
@Autowired
private UserRepository userRepository; // 订单服务直接访问用户数据
@Autowired
private InventoryRepository inventoryRepository; // 直接访问库存数据
}
// ✅ 正确:通过接口隔离跨服务调用
public class OrderService {
@Autowired
private UserServiceClient userClient; // 通过 Feign 调用用户服务
@Autowired
private InventoryServiceClient inventoryClient; // 通过 Feign 调用库存服务
// 领域内的依赖
@Autowired
private OrderRepository orderRepository;
}
// 更进一步:用端口适配器模式隔离外部依赖
// 应用层不依赖具体实现,只依赖接口
public class CreateOrderUseCase {
private final OrderRepository orderRepository;
private final UserPort userPort; // 接口
private final InventoryPort inventoryPort; // 接口
public CreateOrderUseCase(OrderRepository orderRepository,
UserPort userPort,
InventoryPort inventoryPort) {
this.orderRepository = orderRepository;
this.userPort = userPort;
this.inventoryPort = inventoryPort;
}
public Order execute(CreateOrderCommand command) {
// 通过端口查询用户信息
UserInfo user = userPort.getUserInfo(command.getUserId());
// 通过端口预占库存
inventoryPort.reserveStock(command.getSkuId(), command.getQuantity());
// 创建订单(纯领域逻辑,不依赖外部)
Order order = Order.create(command, user);
orderRepository.save(order);
return order;
}
}三大踩坑实录
坑一:按技术层拆分而不是按业务拆分
现象: 团队把系统拆成了"用户基础服务"、"订单基础服务"、"商品基础服务",每个"基础服务"只提供 CRUD 接口,没有任何业务逻辑。业务逻辑全部在一个叫"业务编排服务"的巨型服务里,最终这个服务变成了新的单体。
原因: 按技术层(service / dao / controller)拆分,而不是按业务边界拆分。
解法: 每个微服务必须包含完整的业务逻辑,从 Controller 到 Domain 再到 DB,形成一个完整的"垂直切片"。
坑二:共享数据库
现象: 所有微服务共用一个 MySQL 实例,只是表名不同。A 服务直接 JOIN 了 B 服务的表,导致 A、B 无法独立演进——一旦 B 改表结构,A 也要同步改。
原因: 数据库改造难度大(数据迁移、代码修改),团队选择了妥协。
解法: 共享数据库是技术债,必须还。分步骤:先在代码层面实现逻辑隔离(通过 API 而不是 JOIN),再做物理分离。可以接受有一段时间两个服务的数据库物理上在一起,但逻辑上不互相访问对方的表。
坑三:拆分时机太晚
现象: 系统已经运行了 4 年,数据库 300+ 张表,代码 50 万行,这时才开始拆分,改造周期无限延长,团队精力全消耗在拆分上,新功能没法做。
原因: 早期没有考虑可演进性,等系统大到一定程度再拆,代价极高。
解法: 最佳实践是"绞杀者模式(Strangler Fig Pattern)":不重写,而是在旧系统外面逐步建立新服务,将流量从旧系统迁移到新服务,最终旧系统被"绞杀"消失。关键是要有明确的拆分路线图,每次只迁移一个有明确边界的功能。
写在最后
微服务拆分成功的标志不是服务数量,而是:每个服务可以独立部署、独立扩展、独立演进。如果不满足这三条,拆分就没有意义。
老方后来重新梳理了系统,用 DDD 重新定义了 8 个核心领域,把原来 30 多个服务合并重构成了真正有独立边界的 12 个服务,效果反而比 30 个更好——发版频率从一个月一次变成了每周独立发版。
少即是多,有时候拆分的勇气,不如合并的智慧。
