3、服务拆分原则:领域驱动设计(DDD)、限界上下文、拆分粒度控制
服务拆分这件事,我做了快十年了。说实话,见过太多团队一上来就拍脑袋拆服务,结果拆完发现比没拆还痛苦。今天咱们聊聊怎么科学地拆,而不是瞎拆。
3.1 为什么需要领域驱动设计?
先问个问题:你拆服务的依据是什么?
很多人会说「按功能模块拆」。嗯,这听起来没错,但实际操作中你会发现——「用户管理」和「订单管理」到底算不算两个独立领域?用户下单后,用户信息变了,订单要不要跟着变?
这就是典型的边界模糊问题。我个人习惯用DDD来解决这个痛点。
核心思想:DDD不是技术框架,而是一种思维方式。它要求你从业务视角出发,把系统拆成一个个「有边界的业务领域」。
我在项目中遇到过最典型的反面案例:一个电商系统,把「商品」和「库存」拆成了两个服务。结果每次下单都要跨服务调用,延迟从2ms飙到50ms。为什么?因为从业务角度看,商品和库存本来就是同一个领域内的东西,硬拆开反而制造了问题。
3.2 限界上下文:服务的「护城河」
限界上下文,说白了就是给每个服务画个圈。圈内的事,服务自己说了算;圈外的事,通过接口沟通。
怎么画这个圈?我总结了三个原则:
- 业务完整性:一个限界上下文应该覆盖一个完整的业务能力。比如「订单上下文」包含创建订单、修改订单、取消订单,但不包含支付——支付是另一个上下文。
- 数据一致性:同一个上下文内的数据,尽量保持强一致性;跨上下文的数据,接受最终一致性。
- 变化频率:经常一起变化的功能,放在同一个上下文里。你想想看,如果商品属性和商品价格经常一起改,你非把它们拆到两个服务里,每次改价还得发个消息通知商品服务,这不是自找麻烦吗?
我的小技巧:画限界上下文时,拿张白纸,把业务场景里的名词全部列出来。然后问自己:「这些名词之间,谁对谁负责?」比如「订单」对「商品」负责吗?不,订单只关心商品ID和价格,不关心商品描述。所以它们可以不在同一个上下文里。
3.3 拆分粒度控制:拆多细才算合适?
这是最让人头疼的问题。拆太粗,跟单体没区别;拆太细,分布式带来的复杂度会让你崩溃。
我见过一个团队,把用户服务拆成了「用户基本信息服务」「用户地址服务」「用户偏好服务」三个微服务。结果呢?每次查询用户详情,要调三个接口,网络开销翻了三倍。更惨的是,用户注册时,三个服务的事务根本没法保证一致性。
避坑指南:我曾经犯过一个错误——为了「微」而微,把一个小功能拆成独立服务。后来发现,这个服务每天只有几百次调用,却要维护一套完整的部署、监控、日志体系。得不偿失。
那么,到底拆多细?我一般用这个表格来判断:
| 判断维度 | 适合拆成独立服务 | 适合合并 |
|---|---|---|
| 业务独立性 | 可以独立演进,不依赖其他服务 | 频繁跨服务调用 |
| 数据变更频率 | 变化节奏差异大 | 经常一起变更 |
| 团队规模 | 有独立团队维护(2-5人) | 团队人数少于10人 |
| 性能要求 | 需要独立扩缩容 | 调用链路过长导致延迟 |
举个例子。我之前做的一个支付系统,一开始把「支付」「退款」「对账」拆成了三个服务。后来发现,退款和支付其实共享同一个资金账户模型,每次退款都要查支付状态,跨服务调用频繁。最后我把它们合并成一个「资金服务」,内部用模块隔离,对外只暴露一个接口。性能提升了30%,代码量反而减少了。
3.4 实战:一个订单系统的拆分案例
咱们拿一个真实的订单系统来说说。假设业务包含:用户下单、支付、发货、售后。
按照DDD的思路,我首先识别出几个核心领域:
- 订单领域:负责订单的创建、状态流转、查询
- 支付领域:处理支付请求、回调、退款
- 物流领域:发货、配送、签收
- 售后领域:退货、换货、退款审核
然后画限界上下文。注意,这里有个关键点:退款这个功能,到底属于支付领域还是售后领域?
我的判断是:退款操作本身(扣钱、退钱)属于支付领域,但退款申请、审核流程属于售后领域。所以两个上下文之间通过事件驱动来协作——售后发起退款申请,支付领域监听事件后执行退款操作。
代码示例:事件驱动的边界通信
// 售后领域发布退款事件
public class AfterSaleService {
@Autowired
private EventPublisher eventPublisher;
public void approveRefund(String orderId) {
// 审核通过
RefundApprovedEvent event = new RefundApprovedEvent(orderId);
eventPublisher.publish(event);
}
}
// 支付领域监听退款事件
public class PaymentService {
@EventListener
public void handleRefundApproved(RefundApprovedEvent event) {
// 执行退款操作
paymentGateway.refund(event.getOrderId());
}
}
这样拆完,每个服务的内聚性很高,服务之间的耦合度很低。而且每个服务可以独立部署、独立扩缩容。比如大促期间订单量暴增,我只扩容订单服务就行,支付和物流服务不用动。
3.5 总结一下
服务拆分没有银弹。DDD和限界上下文给你提供了思考框架,但最终粒度控制还是要靠经验和对业务的理解。
我个人建议:先粗后细。刚开始拆的时候,宁可粗一点,也别拆太碎。运行一段时间后,根据实际的调用频率、变更频率、团队协作情况,再决定要不要进一步拆分。毕竟,合并服务比拆分服务难多了——你想想看,两个已经独立运行的服务,要合并成一个,数据迁移、接口兼容、历史数据一致性,哪个不是大坑?
最后一个小提醒:如果你发现一个服务里,有超过一半的接口都是被其他服务调用的,那说明这个服务的边界可能画错了。它应该被合并到调用方那边去。
嗯,今天就聊到这儿。下一章咱们聊聊服务间的通信方式——RPC和消息队列,到底该怎么选。