第2章:服务拆分原则:领域驱动设计(DDD)基础、限界上下文、聚合与聚合根、拆分实战案例

好,咱们进入正题。微服务怎么拆?这是每个团队都会头疼的问题。我见过太多项目,一上来就按功能模块切,结果切完发现服务之间互相调来调去,比单体还乱。说白了,拆分不是拍脑袋,得有方法论。我个人最推崇的,就是领域驱动设计(DDD)。

2.1 领域驱动设计(DDD)基础

DDD 是什么?别被名字吓到。它其实就是一套帮我们理清业务逻辑的方法论。核心思想就一句话:代码要反映业务。你想想看,如果业务人员说「我们要搞一个订单」,你代码里却没有 Order 这个类,那肯定有问题。

DDD 有两个重要阶段:

  • 战略设计:关注大方向,比如哪些是核心业务,哪些是支撑业务。
  • 战术设计:关注具体实现,比如实体、值对象、聚合这些概念怎么落地。

我在项目中遇到过最典型的场景:业务方说「我们要做一个电商平台」。如果直接开干,大概率会做成「订单模块」、「商品模块」、「用户模块」这种技术视角的拆分。但 DDD 会先问:电商的核心领域是什么? 是交易?是库存?还是支付?想清楚这个,拆分才有方向。

核心要点:DDD 不是代码规范,是思维模型。它让你从「技术实现」转向「业务建模」。

2.2 限界上下文:服务的边界线

限界上下文(Bounded Context)是 DDD 里最重要的概念之一。我甚至觉得,理解它,你就理解了微服务拆分的精髓。

什么是限界上下文?说白了,就是一个业务概念的边界。同一个词,在不同上下文里含义完全不同。举个例子:

上下文 「用户」的含义
订单上下文 用户 = 买家,有收货地址、支付方式
客服上下文 用户 = 投诉者,有历史工单、满意度
营销上下文 用户 = 潜在客户,有消费偏好、活跃度

你看,同样是「用户」,三个上下文里关注的点完全不同。如果强行用一个 User 对象通吃所有场景,那这个类会变得臃肿不堪,而且每次改动都牵一发动全身。

我的经验:我曾经在一个项目中,把「商品」概念拆成了「商品上下文」和「库存上下文」。商品上下文管标题、描述、图片;库存上下文管数量、仓库位置。两个服务各自独立演进,互不干扰。这就是限界上下文的威力。

怎么识别限界上下文?我建议你从业务事件入手。比如:

  • 「用户下单」—— 这属于订单上下文
  • 「仓库发货」—— 这属于库存上下文
  • 「财务对账」—— 这属于支付上下文

每个事件,就是一个上下文的边界。

2.3 聚合与聚合根:数据一致性的守护者

好,限界上下文划好了,接下来要处理内部结构。这里就要引入聚合(Aggregate)和聚合根(Aggregate Root)的概念。

聚合是什么?是一组必须保持一致性的对象集合。聚合根是这组对象的入口,外部只能通过聚合根来操作内部对象。

我举个例子,电商里的订单:

// 聚合根:Order
public class Order
{
    public string OrderId { get; private set; }
    public string CustomerId { get; private set; }
    private List<OrderItem> _items = new();

    // 外部只能通过 AddItem 来添加商品
    public void AddItem(string productId, int quantity, decimal price)
    {
        // 业务规则:不能添加数量为0的商品
        if (quantity <= 0)
            throw new DomainException("数量必须大于0");

        _items.Add(new OrderItem(productId, quantity, price));
    }

    // 内部实体:OrderItem
    private class OrderItem
    {
        public string ProductId { get; }
        public int Quantity { get; }
        public decimal Price { get; }

        public OrderItem(string productId, int quantity, decimal price)
        {
            ProductId = productId;
            Quantity = quantity;
            Price = price;
        }
    }
}

注意看,OrderItem 是私有类,外部根本看不到它。你想改订单里的商品?只能通过 Order.AddItem() 方法。这就保证了数据一致性——不会出现订单总金额和商品明细对不上的情况。

避坑指南:我曾经见过一个团队,把聚合根设计得特别大,一个 Order 里塞了十几张表。结果每次查询都要加载一堆无关数据,性能惨不忍睹。记住:聚合要小,越小越好。一个聚合通常只包含 3-5 个实体就够了。

聚合的设计原则:

  • 边界内保证强一致性:一个事务只修改一个聚合
  • 边界外保证最终一致性:跨聚合的操作用事件驱动
  • 聚合根是唯一入口:不能绕过聚合根直接操作内部对象

2.4 拆分实战案例:从电商订单说起

理论说完了,咱们来点实战。假设我们要做一个电商系统,怎么用 DDD 来拆分?

第一步,识别限界上下文。我通常会拉上业务方,一起画一张上下文地图

  1. 商品上下文:管理商品信息、分类、品牌
  2. 订单上下文:管理购物车、下单、订单状态
  3. 库存上下文:管理库存数量、仓库调拨
  4. 支付上下文:管理支付单、退款
  5. 物流上下文:管理发货、配送、签收

第二步,每个上下文内部,再识别聚合。以订单上下文为例:

聚合 聚合根 包含实体
购物车聚合 ShoppingCart CartItem
订单聚合 Order OrderItem, OrderAddress
支付聚合 Payment PaymentLog

第三步,确定服务边界。一个限界上下文,通常对应一个微服务。但也不是绝对的——如果某个上下文特别简单,也可以合并。我个人习惯是:先拆得细一点,再根据实际情况合并。拆容易,合也容易,但一开始就拆大了,后面想拆就难了。

实战要点:

  • 每个微服务有自己的数据库,不要共享
  • 服务间通信用事件或API,不要直接访问数据库
  • 聚合根是服务对外的唯一入口

嗯,这里要注意一点:拆分不是一蹴而就的。我见过很多团队,花了两周画了一堆漂亮的 DDD 图,结果代码写出来还是老样子。为什么?因为DDD 需要持续迭代。业务在变,你的模型也要跟着变。别怕重构,好的架构是改出来的。

最后,送大家一句话:微服务拆分的本质,不是技术问题,是业务问题。把业务理清了,技术实现就是水到渠成的事。