第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 来拆分?
第一步,识别限界上下文。我通常会拉上业务方,一起画一张上下文地图:
- 商品上下文:管理商品信息、分类、品牌
- 订单上下文:管理购物车、下单、订单状态
- 库存上下文:管理库存数量、仓库调拨
- 支付上下文:管理支付单、退款
- 物流上下文:管理发货、配送、签收
第二步,每个上下文内部,再识别聚合。以订单上下文为例:
| 聚合 | 聚合根 | 包含实体 |
|---|---|---|
| 购物车聚合 | ShoppingCart | CartItem |
| 订单聚合 | Order | OrderItem, OrderAddress |
| 支付聚合 | Payment | PaymentLog |
第三步,确定服务边界。一个限界上下文,通常对应一个微服务。但也不是绝对的——如果某个上下文特别简单,也可以合并。我个人习惯是:先拆得细一点,再根据实际情况合并。拆容易,合也容易,但一开始就拆大了,后面想拆就难了。
实战要点:
- 每个微服务有自己的数据库,不要共享
- 服务间通信用事件或API,不要直接访问数据库
- 聚合根是服务对外的唯一入口
嗯,这里要注意一点:拆分不是一蹴而就的。我见过很多团队,花了两周画了一堆漂亮的 DDD 图,结果代码写出来还是老样子。为什么?因为DDD 需要持续迭代。业务在变,你的模型也要跟着变。别怕重构,好的架构是改出来的。
最后,送大家一句话:微服务拆分的本质,不是技术问题,是业务问题。把业务理清了,技术实现就是水到渠成的事。