写在前面
上一篇讲到,模块化单体的关键是画对边界,而真正的边界是数据边界。这一篇就把这个问题彻底说透:服务(或模块)到底该按什么切?
这是整个架构系列里最关键的一篇。拆分边界画错了,比不拆还惨——因为错误边界上的每一次跨服务调用,都是一次跨网络的分布式事务。
一、三种常见的拆分思路
1
2
3
4
5
6
7
8
9
10
11
| 思路 A:按技术层切(最常见、最错误)
用户服务 = 所有用户的 Controller/Service/Dao
── 错。这是把单体里的"层"竖着切了,制造了一堆跨层网络调用。
思路 B:按数据表切
订单服务 = 订单表相关的一切
── 接近正确,但只看数据不看业务,容易切碎。
思路 C:按业务领域切(正确答案)
订单服务 = "下单"这个业务能力涉及的全部(代码 + 数据)
── 这就是 DDD(领域驱动设计)的限界上下文。
|
二、为什么不能按技术层切
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 按技术层切的灾难:
假设拆成:Controller 服务、Service 服务、DAO 服务
一次"下单"要跨 3 个服务调用:
API → Controller 服务 → Service 服务 → DAO 服务 → DB
问题:
✗ 一次业务操作 = 3 次网络往返(慢)
✗ 任意一层挂了,整个链路挂(脆弱)
✗ 改一个业务规则要动 3 个服务(没解耦)
✗ 事务跨 3 个服务(分布式事务地狱)
本质:技术层是"同一个业务的不同实现细节",
把实现细节切成独立服务,是把内部的刀子变成了网络上的刀子。
|
铁律:服务的边界必须沿着业务能力切,绝不能沿着技术分层切。
三、按领域切:DDD 限界上下文
领域驱动设计(DDD)给出了正确的拆分单位——限界上下文(Bounded Context)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 限界上下文 = 一个业务子领域的"自治范围"
在这个范围内:
- 有自己的领域模型(术语、实体、规则)
- 有自己的数据
- 对外提供清晰的业务能力接口
电商的典型限界上下文:
- 订单上下文(下单、支付、退款)
- 商品上下文(上架、库存、SKU)
- 用户上下文(注册、登录、资料)
- 配送上下文(发货、物流、签收)
每个"上下文"就是一个天然的模块/服务候选。
|
3.1 怎么找限界上下文
1
2
3
4
5
6
7
8
9
10
11
12
| 方法 1:事件风暴(Event Storming)
把业务流程里发生的"领域事件"全列出来
(订单已创建、已支付、已发货……)
按相关性聚类 → 一簇事件 ≈ 一个上下文
方法 2:语言分析法
同一个词在不同语境下含义不同,就是上下文边界
例:"商品"
- 在商品上下文:是指上架的 SKU(有详情、图片)
- 在订单上下文:是指下单时的快照(有当时价格)
- 在配送上下文:是指要发货的物理货品(有重量、体积)
→ 三个"商品",属于三个上下文,不能硬塞一个模型。
|
3.2 一个概念,多个模型
1
2
3
4
5
6
7
8
9
10
11
| 新手错误:追求"一个全局统一的 User/Product 模型"
→ 模型越塞越大,字段来自所有部门,谁都改不动。
DDD 的正确姿势:每个上下文只保留自己需要的视图
User 在「用户上下文」:id, 手机号, 密码, 注册时间
User 在「订单上下文」:id, 收货地址, 会员等级(折扣用)
User 在「配送上下文」:id, 收货地址, 联系电话
字段不重复冗余存储?不——必要时冗余(下一篇数据一致性会讲)
关键是:模型是上下文私有的,不互相污染。
|
四、拆错了的代价
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 错误拆分的典型症状(出现这些,说明边界画错了):
✗ 跨服务调用密集
一个请求要链式调 5、6 个服务 → 拆得太碎或边界错了
✗ 共享数据库
多个服务读写同一张表 → 边界根本没切干净
✗ 频繁的分布式事务
动不动就要保证"跨服务的数据一致" → 这些本该在一个服务内
✗ 改一个需求要动多个服务
本来一个模块内的改动,被迫协调多个团队
✗ 服务间循环依赖
A 调 B,B 又调 A → 边界划分反了
|
判断边界好坏的最简单标准:高频一起变更的东西,应该在同一个服务里。变更频率和方向的耦合度,是验证边界的试金石。
五、拆分粒度:多大算合适
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 太大:一个服务干所有事 → 退回单体(但有了网络成本)
太小:每个 API 一个服务 → "纳米服务",运维灾难
实用经验:
1. 能由"一个小团队(2~3 人 Pizza Team)独立负责"为一个单位
→ 这就是"两个披萨团队"原则的由来
2. 一个业务能力(动词)对应一个服务,而不是一个名词
"订单管理"是好服务名;"订单表"不是
3. 先粗后细
先按大领域切 4~6 个粗粒度服务,
跑稳了,哪个真的扛不住再二次拆分。
一次拆太细几乎必然拆错。
4. 别为了"扩容"拆服务
扩容可以用多实例解决(无状态水平扩展),
不需要靠拆服务。拆服务是为了"解耦",不是"扩容"。
|
六、一个实战例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 电商"下单"流程,看正确边界怎么让事情变简单:
错误边界(按技术层 / 按表):
下单 = 调「订单服务」+「库存服务」+「价格服务」+「优惠券服务」
全程分布式事务,任何一个失败都要回滚,痛苦。
正确边界(按领域聚合):
下单逻辑收敛在「订单上下文」内部:
- 查商品快照、扣库存、算价格 都在订单服务内(同库事务)
- 只在"成功后"异步通知「库存」「积分」服务(最终一致)
一次本地事务搞定核心,周边用事件解耦。
区别:
核心交易 → 强一致,收敛在一个服务内
周边副作用(积分、通知、统计)→ 异步、最终一致
|
这就引出第五篇的核心:核心数据强一致收敛在服务内,跨服务用最终一致性。
七、小结
- 三种拆法:按技术层(错)、按表(接近)、按领域(对)
- 铁律:服务边界沿业务能力切,绝不沿技术分层切
- DDD 限界上下文:业务子领域的自治范围,是天然的拆分单位
- 一概念多模型:每个上下文只保留自己需要的视图,不要追求全局统一模型
- 边界好坏的试金石:高频一起变更的,应在同一服务;出现密集跨服务调用/共享库/循环依赖,说明拆错了
- 粒度:一个小团队能独立负责为单位,先粗后细,别为扩容而拆
下一篇算一笔明白账:微服务的"分布式税"——拆服务之后,你到底要为哪些隐性成本买单。