第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是工具,不是信仰。

一句话总结:先有业务边界,再有服务拆分。限界上下文是服务的国境线,子域划分是资源的分配策略。别让技术牵着业务走,要让业务指导技术落地。