后端架构实战(三):服务拆分的第一性原理——按领域切,不是按技术切

写在前面

上一篇讲到,模块化单体的关键是画对边界,而真正的边界是数据边界。这一篇就把这个问题彻底说透:服务(或模块)到底该按什么切

这是整个架构系列里最关键的一篇。拆分边界画错了,比不拆还惨——因为错误边界上的每一次跨服务调用,都是一次跨网络的分布式事务。


一、三种常见的拆分思路

 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 限界上下文:业务子领域的自治范围,是天然的拆分单位
  • 一概念多模型:每个上下文只保留自己需要的视图,不要追求全局统一模型
  • 边界好坏的试金石:高频一起变更的,应在同一服务;出现密集跨服务调用/共享库/循环依赖,说明拆错了
  • 粒度:一个小团队能独立负责为单位,先粗后细,别为扩容而拆

下一篇算一笔明白账:微服务的"分布式税"——拆服务之后,你到底要为哪些隐性成本买单。