第3章:服务拆分原则:领域驱动设计(DDD)入门、限界上下文与子域划分、服务拆分实战案例
3.1 为什么我们需要DDD?
说实话,我见过太多微服务项目死在「拆分」这一步。
团队一上来就拍脑袋:「订单拆一个服务,用户拆一个服务,商品拆一个服务」。结果呢?三个月后,订单服务里塞满了支付逻辑,用户服务里混着营销规则。服务之间互相调用,改一个功能要拉五个团队开会。
为什么会这样?
因为大家只看到了「技术拆分」,没看到「业务边界」。而领域驱动设计(DDD),就是帮你找到那个边界的。
我个人习惯把DDD理解成一套「业务建模语言」。它不关心你用PHP还是Go,不关心你用MySQL还是MongoDB。它只关心一件事:你的业务到底长什么样?
核心观点:DDD不是架构方法论,而是业务建模方法论。先有业务模型,才有技术架构。
3.2 限界上下文:服务的「国境线」
DDD里最重要的概念,我个人觉得是「限界上下文」。你想想看,一个国家有国境线,国境线内法律统一、语言统一。微服务也一样——每个服务应该有自己的「国境线」。
举个例子。电商系统里,「商品」这个词在不同上下文里含义完全不同:
- 商品上下文:商品有SKU、库存、价格、属性
- 营销上下文:商品有折扣、满减、优惠券适用规则
- 物流上下文:商品有重量、体积、是否易碎
如果你把这三个「商品」塞进同一个服务,那这个服务就会变得臃肿不堪。我在项目中遇到过这样的案例——一个「商品服务」维护了80多个字段,每次上线都提心吊胆,因为改一个字段可能影响三个业务方。
我的经验:判断限界上下文是否合理,有一个简单方法——如果两个团队对同一个名词的理解不一致,那它们就应该属于不同的上下文。
3.3 子域划分:核心、支撑、通用
有了限界上下文,接下来要划分子域。DDD把子域分成三类:
| 子域类型 | 定义 | 例子 | 策略 |
|---|---|---|---|
| 核心域 | 公司最赚钱、最核心的业务 | 电商的订单、支付 | 自研,投入最多资源 |
| 支撑域 | 为核心域服务,但非核心竞争力 | 权限管理、消息通知 | 自研,但可适当简化 |
| 通用域 | 行业通用,没有业务特色 | 邮件发送、短信、日志 | 直接采购或使用开源 |
嗯,这里要注意。很多团队犯的错误是——把核心域和支撑域混在一起。我曾经见过一个团队,把「订单」和「短信通知」放在同一个服务里。结果短信服务挂了,订单也下不了。这就是典型的边界没划清楚。
说白了,核心域是你的命根子,要投入最好的资源。通用域能用现成的就用现成的,别自己造轮子。
3.4 服务拆分实战案例:从订单系统说起
光讲理论没意思。我们来看一个真实的案例。
假设我们要做一个电商订单系统。按照传统思路,可能会拆成:
- 订单服务
- 支付服务
- 库存服务
- 物流服务
但用DDD的思路,我们要先做业务建模。
第一步:识别限界上下文
我习惯先拉上产品、运营、开发一起开个「事件风暴」工作坊。大家把业务事件写在便利贴上,比如:
- 用户下单
- 支付成功
- 库存扣减
- 订单发货
- 用户取消订单
然后把这些事件归类,找出它们所属的上下文。最终我们得到:
订单上下文:下单、取消、查询
支付上下文:支付、退款、对账
库存上下文:扣减、回滚、预警
物流上下文:发货、签收、退货
第二步:定义聚合与实体
每个上下文里,我们要找到「聚合根」。比如订单上下文里,「订单」就是聚合根。所有对订单的操作,都要通过订单聚合根来完成。
// 订单聚合根示例
class Order {
private string $orderId;
private OrderStatus $status;
private array $items; // 订单项
private Money $totalAmount;
private Address $shippingAddress;
public function place(): void {
// 校验库存
// 计算金额
// 生成订单号
$this->status = OrderStatus::PLACED;
// 发布订单已创建事件
EventBus::publish(new OrderPlaced($this->orderId));
}
public function cancel(): void {
if ($this->status !== OrderStatus::PLACED) {
throw new \DomainException('只有待支付的订单才能取消');
}
$this->status = OrderStatus::CANCELLED;
EventBus::publish(new OrderCancelled($this->orderId));
}
}
避坑指南:我曾经犯过一个错误——把「订单项」也当成聚合根。结果订单和订单项之间频繁跨服务调用,性能一塌糊涂。记住:聚合根是事务一致性的边界,一个事务只修改一个聚合根。
第三步:确定服务边界
有了限界上下文和聚合根,服务拆分就水到渠成了:
- 订单服务:负责订单的创建、取消、查询。内部包含Order聚合根
- 支付服务:负责支付、退款。内部包含Payment聚合根
- 库存服务:负责库存扣减、回滚。内部包含Inventory聚合根
- 物流服务:负责发货、签收。内部包含Shipment聚合根
服务之间通过事件进行异步通信。比如订单服务发布「OrderPlaced」事件,库存服务监听后扣减库存,支付服务监听后发起支付流程。
3.5 总结:DDD不是银弹
最后说点实在的。DDD确实能帮你理清业务边界,但它不是银弹。
我见过一些团队,为了DDD而DDD,把简单的CRUD系统硬生生拆成十几个服务,结果维护成本翻了好几倍。你想想看,一个只有三个开发的小团队,搞什么事件风暴、聚合根、领域事件?老老实实单体应用先跑起来不好吗?
我个人建议:
- 业务复杂度不高时,别用DDD,别拆微服务
- 业务复杂度高时,先用DDD建模,再决定是否拆分
- 拆分时,先从核心域开始,支撑域和通用域可以晚点再拆
记住:微服务是手段,不是目的。DDD是工具,不是信仰。
一句话总结:先有业务边界,再有服务拆分。限界上下文是服务的国境线,子域划分是资源的分配策略。别让技术牵着业务走,要让业务指导技术落地。