编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

  • 常见设计原则

  • 巧学设计模式

  • 系统架构设计

    • README
    • 分层架构设计详解
    • 六边形架构设计
    • 命令查询职责分离
    • 事件驱动架构设计
    • 微服务拆分策略
      • 1. 案例引入
        • 1.1 一拆就崩在哪
        • 1.2 顺藤摸到根因
        • 1.3 我们要回答什么
      • 2. 架构概览
        • 2.1 微服务体系总图
        • 2.2 为什么必须拆
      • 3. 单体到微服务
        • 3.1 单体的临界点
        • 3.2 SOA与微服务之别
        • 3.3 康威定律的反作用
        • 3.4 渐进式拆分路径
      • 4. 按业务能力拆分
        • 4.1 业务能力地图
        • 4.2 能力边界识别
        • 4.3 高内聚低耦合标尺
        • 4.4 反模式实体服务化
      • 5. 限界上下文拆分
        • 5.1 通用语言的歧义
        • 5.2 上下文边界识别
        • 5.3 上下文映射九模式
        • 5.4 子域分类与战略选择
      • 6. 服务粒度权衡
        • 6.1 拆细的代价
        • 6.2 拆粗的代价
        • 6.3 粒度判断五维表
        • 6.4 单体优先原则
      • 7. 数据库拆分策略
        • 7.1 共享数据库反模式
        • 7.2 一库一服务
        • 7.3 跨库查询的解法
        • 7.4 分布式事务的取舍
      • 8. 拆分中的陷阱
        • 8.1 分布式单体
        • 8.2 网状调用爆炸
        • 8.3 共享模型回头路
        • 8.4 五大反模式集锦
      • 9. 落地实战剖析
        • 9.1 事件风暴工作坊
        • 9.2 绞杀者迁移模式
        • 9.3 拆分顺序选择
        • 9.4 拆分成熟度评估
      • 10. 综合案例串讲
        • 10.1 案例真相揭晓
        • 10.2 一次拆分的全过程
        • 10.3 设计哲学回扣
        • 10.4 拆分决策速查
    • 领域驱动战略设计
    • 架构评审方法论
    • 架构演进实战指南
  • 编程
  • 系统架构设计
杨充
2017-02-19
目录

微服务拆分策略

# 微服务拆分策略

# 目录介绍

  • 1. 案例引入
    • 1.1 一拆就崩在哪
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 微服务体系总图
    • 2.2 为什么必须拆
  • 3. 单体到微服务
    • 3.1 单体的临界点
    • 3.2 SOA与微服务之别
    • 3.3 康威定律的反作用
    • 3.4 渐进式拆分路径
  • 4. 按业务能力拆分
    • 4.1 业务能力地图
    • 4.2 能力边界识别
    • 4.3 高内聚低耦合标尺
    • 4.4 反模式实体服务化
  • 5. 限界上下文拆分
    • 5.1 通用语言的歧义
    • 5.2 上下文边界识别
    • 5.3 上下文映射九模式
    • 5.4 子域分类与战略选择
  • 6. 服务粒度权衡
    • 6.1 拆细的代价
    • 6.2 拆粗的代价
    • 6.3 粒度判断五维表
    • 6.4 单体优先原则
  • 7. 数据库拆分策略
    • 7.1 共享数据库反模式
    • 7.2 一库一服务
    • 7.3 跨库查询的解法
    • 7.4 分布式事务的取舍
  • 8. 拆分中的陷阱
    • 8.1 分布式单体
    • 8.2 网状调用爆炸
    • 8.3 共享模型回头路
    • 8.4 五大反模式集锦
  • 9. 落地实战剖析
    • 9.1 事件风暴工作坊
    • 9.2 绞杀者迁移模式
    • 9.3 拆分顺序选择
    • 9.4 拆分成熟度评估
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次拆分的全过程
    • 10.3 设计哲学回扣
    • 10.4 拆分决策速查

# 1. 案例引入

# 1.1 一拆就崩在哪

先看一段在真实公司发生过的"微服务化失败"——一家电商团队 30 人,原本是一个 Spring Boot 单体,QPS 5000,运转良好。CTO 听了几场架构大会后拍板"微服务化",三个月后拆出 18 个服务:

用户服务      商品服务      库存服务      价格服务      优惠服务
订单服务      支付服务      物流服务      地址服务      发票服务
评价服务      搜索服务      推荐服务      消息服务      通知服务
会员服务      积分服务      报表服务
1
2
3
4

拆完之后的现象:

  • 单接口 P99 延迟:从 80ms 涨到 1.2s
  • 上线发布:从每周 5 次降到每月 2 次(每次要协调 6+ 个团队)
  • 故障率:从月均 1 次涨到周均 3 次(任何一个服务挂,下单全链路崩)
  • 团队人效:新人上手时间从 2 周变成 2 个月(要看完 18 个服务才能改一行代码)

最荒诞的一幕:一个"用户下单"接口的调用链路:

API网关 → 订单服务 → 用户服务(拿用户信息)
                  → 商品服务(拿商品信息)→ 价格服务(算价格)→ 优惠服务(算优惠)
                  → 库存服务(扣库存)
                  → 地址服务(拿收货地址)
                  → 支付服务(创建支付单)
                  → 消息服务(发下单消息)→ 通知服务(推 push)
                  → 积分服务(预扣积分)

一次下单:12 个跨进程 RPC + 8 次跨库事务
1
2
3
4
5
6
7
8
9

测试环境跑通了,上生产那天双 11 零点流量一来,整个体系级联雪崩——某个二级服务超时,订单服务等不到响应也超时,前端无限重试,连带网关被压垮。

直觉怀疑:是不是 RPC 框架不行?换到 gRPC?换到服务网格?——团队折腾了半年,性能没起色,反而把服务网格、配置中心、链路追踪、熔断限流的运维负担全背上了。

最后做事故复盘,结论只有一句:

不是技术不行,是从一开始就不该这么拆。

# 1.2 顺藤摸到根因

带着这条线往下挖:

  • 假设 1:是不是 RPC 性能问题?—— gRPC 单跳 P99 也就 5ms,12 跳串行才 60ms,跟单体内调用相比慢 10 倍是不可避免的物理代价
  • 假设 2:是不是服务太多?—— 18 个服务对应 30 人团队,人均 0.6 个服务,平均一个服务连一个全职 owner 都没有
  • 假设 3:为什么"用户服务"会被订单调?—— 因为订单要"用户昵称、头像、等级"等字段;这些字段每次下单都拉一遍,本质是用 RPC 模拟了一次 SQL JOIN
  • 假设 4:为什么"价格"和"优惠"要分开?—— 因为最初设计文档里"价格"和"优惠"是两个章节;文档结构变成了服务结构,但业务上它们是一回事——"算出最终价"
  • 假设 5:为什么任何服务挂下单都崩?—— 因为 12 个 RPC 都是同步强依赖,任何一个超时都阻塞主链路;本应该是"消息异步发出"的,被设计成"必须等回包"
  • 假设 6:为什么测试环境通过?—— 测试环境数据少、调用快、并发低,根本暴露不出网状依赖的级联问题
  • 假设 7:为什么改回去这么难?—— 因为 18 个服务对应 18 个 Git 仓库、18 个数据库、18 套 CI/CD,逆向合并比正向拆分代价更大

看似 "拆服务就是上微服务" 的决策,没毛病在技术选型,毛病在没有意识到"拆分"是一个有边界、有代价、有方法的工程问题——这条事故里至少藏着 7 个原理点:

① 什么时候必须从单体拆? 临界点在哪?                   → 第 3 章
② "按业务能力" vs "按数据实体" 拆,哪个对?              → 第 4 章
③ 限界上下文到底怎么识别? 不是拍脑袋画框?              → 第 5 章
④ 服务粒度多大才合适? 5 个还是 50 个?                  → 第 6 章
⑤ 数据库要不要也拆? 拆了跨库查询怎么办?                → 第 7 章
⑥ 为什么"看似拆开"的服务还是一损俱损?                  → 第 8 章
⑦ 已有单体怎么安全演进? 有方法论吗?                    → 第 9 章
1
2
3
4
5
6
7

# 1.3 我们要回答什么

这个事故就是本篇的主线案例。我们带着上面 7 个问号往下走,每讲完一段原理就解开一两个;最后在第 10 章把案例彻底剖开,并给出三种修复方案与各自的代价。

本篇路线:

架构总图 (第 2 章)
   ↓
单体到微服务 (第 3 章) ─→ 解开"什么时候该拆"
   ↓
按业务能力 → 限界上下文 (第 4-5 章) ─→ 解开"按什么标准拆"
   ↓
粒度权衡 → 数据库拆分 (第 6-7 章) ─→ 解开"拆到什么程度"
   ↓
陷阱集锦 (第 8 章) ─→ 解开"为什么拆完反而更差"
   ↓
落地实战 (第 9 章) ─→ 武器库
   ↓
综合案例 (第 10 章) ─→ 案例彻底剖开
1
2
3
4
5
6
7
8
9
10
11
12
13

📌 本篇定位:这是整个系列的战略决策篇。前面 02-04 篇讲"架构内部如何组织",本篇讲"系统外部如何切分"。读完本篇后,再面对任何"要不要拆服务"的争论,都能立刻回答:"为什么要拆、拆到什么程度、代价是什么"。

# 2. 架构概览

# 2.1 微服务体系总图

我们看一个成熟的微服务体系的全貌(以电商域为例):

                       ┌──────────────────────┐
                       │      API 网关         │  ← 鉴权、限流、路由
                       └──────────┬───────────┘
                                  ▼
              ┌───────────────────────────────────────┐
              │           BFF 层(按端聚合)            │  ← Web BFF / App BFF
              └───┬─────────────┬─────────────┬───────┘
                  ▼             ▼             ▼
        ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
        │  订单上下文   │ │  商品上下文    │ │  会员上下文   │
        │  ┌────────┐  │ │  ┌────────┐  │ │  ┌────────┐  │
        │  │ 下单    │  │ │  │ 商品   │  │ │  │ 用户    │  │
        │  │ 履约    │  │ │  │ 库存   │  │ │  │ 积分    │  │
        │  │ 售后    │  │ │  │ 价格   │  │ │  │ 等级    │  │
        │  └────────┘  │ │  └────────┘  │ │  └────────┘  │
        │   独占数据库  │ │   独占数据库   │ │   独占数据库  │
        └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
               │                │                │
               └────────────────┼────────────────┘
                                ▼
                       ┌────────────────┐
                       │   事件总线      │  ← Kafka 异步解耦
                       └────────────────┘
                                │
            ┌───────────────────┼───────────────────┐
            ▼                   ▼                   ▼
      ┌──────────┐        ┌──────────┐        ┌──────────┐
      │ 支付上下文 │        │ 物流上下文 │        │ 营销上下文 │
      └──────────┘        └──────────┘        └──────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

体系五大层的核心属性速查:

层 职责 边界 通信
API 网关 鉴权、限流、路由 一个网关集群 HTTP
BFF 端的需求聚合 一端一 BFF HTTP/RPC
限界上下文 业务能力 子域边界 RPC + 事件
服务 上下文内的功能 聚合根边界 内部调用
事件总线 异步解耦 全局 MQ

关键认知:微服务的"服务"不是最小单位——限界上下文才是。一个上下文可能包含多个进程,但它们共享一套数据、一个团队、一个发布节奏。

# 2.2 为什么必须拆

为什么要把单体拆成微服务?而不是继续加机器、加缓存?

疑惑:单体性能不够就横向扩容,简单粗暴有效——为什么要付出微服务的复杂度代价?

论证:

  1. 代码规模超过认知极限——50 万行代码以上的单体,没有人能完整理解全貌。新人改一行可能挂掉 5 个不相关的功能。拆分把"全局复杂度"切成多个"局部复杂度"。
  2. 发布节奏被锁死——单体只有一个 CI/CD 流水线,任何一个模块要发版,整个系统要回归。100 人团队挤一个发布窗口,月发布次数 = 1。拆分让各团队独立发布。
  3. 技术栈被锁死——单体只能选一套技术栈。如果某个模块更适合 Go、某个模块需要 Python ML 库,单体里没法选。拆分让每个服务独立选型。
  4. 故障域无法隔离——单体里任何一个 NPE、死锁、内存泄漏都会拖垮整个进程。爆炸半径 = 整个业务。拆分让故障局部化。
  5. 扩容粒度不匹配——商品服务 QPS 10 万,订单服务 QPS 1 万——单体里两者绑死,只能按最高 QPS 扩容,资源浪费 10 倍。拆分让按需扩容。
  6. 团队规模超过 Two-Pizza——单一代码库超过 10 人协作,merge 冲突、需求排期、技术决策全要全员对齐——沟通成本以 N² 增长。拆分让团队独立决策。
  7. 反向验证:如果不拆会怎样?看看那些 200 万行的"祖传单体"——每次发版要全公司同步排期,bug 修复要找历史考古学家,技术栈停留在 10 年前。单体在某个规模之后就是技术债务的复利。

结论:拆分不是为了"赶时髦",而是把代码复杂度、发布节奏、技术选型、故障域、扩容粒度、团队协作这六个独立维度同时解耦——一个服务一种节奏,崩溃定位、独立发布、按需扩容、自主选型全都得益于此。这是大规模分布式系统的根基哲学。

但请注意:上面六条有一条不成立,就不该拆。规模没到、团队没到、节奏没到——拆了反而是负优化。本篇第 6.4 节会反复强调"单体优先"。

下面我们从"什么时候该拆"开始,看清拆分的真正动因。

# 3. 单体到微服务

# 3.1 单体的临界点

疑惑:什么样的单体是"该拆了"的单体?凭感觉拍脑袋吗?

论证:业界沉淀了一组临界指标,达到任意 3 条以上就值得认真考虑拆分:

维度 临界值 信号
代码规模 > 50 万行 全量编译 > 5 分钟
团队规模 > 30 人 单仓库 PR 排队 > 1 天
模块数 > 20 个 改一处影响 5+ 模块
发布频率 < 1 次/周 发布要全员对齐
故障关联度 单点故障影响 > 70% 功能 任何 bug 都全站不可用
QPS 不均衡 模块间相差 10 倍以上 扩容浪费严重
技术栈冲突 出现"想用但用不了" 框架版本卡死

反向指标——出现以下情况不要急着拆:

  • 团队 < 10 人
  • 业务还在快速调整,模型不稳定
  • DevOps、监控、链路追踪基础设施缺失
  • 没有人能讲清"模块边界在哪"

生产经验:

  • 50 人以下团队,单体永远是更优解——加机器、加缓存、垂直分层就够了
  • 50~150 人团队,应该拆 3~5 个上下文级服务——按业务能力切,不要按实体切
  • 150 人以上,才适合 10+ 服务的微服务体系——前提是有专门的平台团队支撑

# 3.2 SOA与微服务之别

疑惑:SOA(面向服务架构)和微服务有什么本质区别?2000 年就有 SOA 了,为什么 2014 年还要造"微服务"这个词?

论证:核心差异在集中 vs 去中心化:

维度 SOA 微服务
通信 重 ESB(企业服务总线) 轻 RPC / HTTP / MQ
数据 共享数据库常见 一库一服务
治理 中心化(统一规范) 去中心化(每团队自治)
协议 SOAP / WSDL REST / gRPC / Kafka
部署 大颗粒、低频 小颗粒、高频(每天数次)
团队 跨团队协调 Two-Pizza 全栈自治
数据契约 XML Schema 强约束 JSON / Proto 弱演进

SOA 的致命问题:ESB 变成了**"分布式单体的中枢"**——所有调用都要过 ESB,ESB 一挂全挂;ESB 团队成为瓶颈,任何服务变动要排队;XML/SOAP 协议臃肿,性能堪忧。

微服务的核心革命:

  1. 去掉中心化总线——服务直接互调,最多有个轻量服务发现
  2. 强制数据隔离——一库一服务,杜绝"通过共享数据库耦合"
  3. 完全自治团队——技术栈、发布节奏、运维方式各团队说了算
  4. 协议轻量化——HTTP/JSON 或 gRPC,没有 SOAP 那种历史包袱

结论:微服务不是 SOA 的升级,是对 SOA 中心化失败的反思后的重新设计。理解这一点,才能避免在落地时把微服务又做成 SOA(典型表现:堆一堆服务网格、ESB-like 中间件,又回到中心化)。

# 3.3 康威定律的反作用

1968 年 Melvin Conway 提出:

任何设计系统的组织,最终产生的设计等同于组织的沟通结构。

这是康威定律——系统结构会自动收敛到团队结构。

正向应用(设计组织 → 系统形态):

团队结构                          系统形态
┌──────────┐                    ┌──────────┐
│ 订单团队  │                    │ 订单服务  │
└──────────┘                    └──────────┘
┌──────────┐                    ┌──────────┐
│ 商品团队  │   →(必然导致)→    │ 商品服务  │
└──────────┘                    └──────────┘
┌──────────┐                    ┌──────────┐
│ 支付团队  │                    │ 支付服务  │
└──────────┘                    └──────────┘
1
2
3
4
5
6
7
8
9
10

反向应用——逆康威(设计系统形态 → 调整组织):

想要某种系统架构?先把组织调成那种结构。

例子:要拆出"会员上下文"和"营销上下文"两个独立服务,先把原"会员营销组"拆成两个独立组,各自有 owner、各自承担 KPI——系统会自然按组织边界拆开。

反作用——本篇第 1 章案例最深的教训:30 人团队硬拆 18 个服务,违反康威定律——

组织:30 人,约 5 个小组
系统:18 个服务

每个服务平均 1.6 人维护——根本不够
→ 服务实际归属混乱,没人对哪个服务真正负责
→ 出问题踢皮球,加需求互相推
→ 系统逐渐腐化成"无主之地"
1
2
3
4
5
6
7

康威定律的工程纪律:

  1. 服务数量上限 ≈ 团队数量——一个团队最多维护 2~3 个服务
  2. 服务边界 = 团队边界——跨团队的服务,永远协调成本最高
  3. 想新增服务?先证明有团队能承接——没人接的服务不是资产,是负债

# 3.4 渐进式拆分路径

拆分必须渐进,绝不能一步到位。标准的演进阶梯:

阶段 1:纯单体
   ├─ 单一代码库
   ├─ 单一数据库
   ├─ 单一进程
   └─ 适用:< 30 人、< 20 万行、QPS < 5000

         ↓ 模块边界开始模糊

阶段 2:模块化单体(Modular Monolith)
   ├─ 单一进程,但内部按上下文分模块(如 Spring Boot 多 module)
   ├─ 模块间通过明确接口调用,禁止跨模块直接读对方数据库
   ├─ 单一数据库,但按模块划分 schema/前缀
   └─ 适用:30~80 人、20~50 万行
   ★ 关键:90% 的"单体之痛"都能在这一阶段解决,不一定要上微服务

         ↓ 发布节奏冲突 / 故障域扩大

阶段 3:粗粒度服务化(3~5 个服务)
   ├─ 按核心上下文(订单、商品、用户)拆 3~5 个服务
   ├─ 每个服务独立数据库、独立部署
   ├─ 通信以 RPC 为主,事件总线为辅
   └─ 适用:80~200 人、QPS > 1 万

         ↓ 团队继续扩大

阶段 4:细粒度微服务(10~30 个服务)
   ├─ 每个上下文内再按聚合根拆 2~5 个服务
   ├─ 引入服务网格、配置中心、链路追踪
   ├─ 事件驱动 + Saga 处理跨服务事务
   └─ 适用:200+ 人、平台团队齐备

         ↓ 业务/组织继续演化

阶段 5:领域平台 + 业务前台
   ├─ 抽出共性能力(用户、订单、支付)作为中台
   ├─ 上层快速搭建业务前台
   └─ 适用:超大型组织、多业务线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

关键纪律:

  • 每一阶段稳定运行 6~12 个月再考虑下一步——给团队消化时间
  • 不要跳级——单体直接跳微服务,失败率超过 80%
  • 可以倒退——发现拆错了就合回去,合并比硬撑代价小

第 1 章案例的失败正是从阶段 1 直接跳到阶段 4——跳过了"模块化单体"这关键一步。

# 4. 按业务能力拆分

# 4.1 业务能力地图

疑惑:拆服务的第一刀该按什么切?按数据表?按部门?按 URL 路径?

论证:业界经过 20 年试错的答案是——按业务能力(Business Capability)。业务能力是"组织提供的、对外可见的、稳定的价值单元"。

电商域典型业务能力地图:

┌─────────────────────────────────────────────────────────┐
│                      电商业务能力                         │
├─────────────────────────────────────────────────────────┤
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │商品管理   │  │交易履约   │  │会员服务   │  │营销活动   │ │
│  │ - 商品发布│  │ - 下单    │  │ - 注册   │  │ - 优惠券  │ │
│  │ - 库存管理│  │ - 支付   │  │ - 登录   │  │ - 满减   │ │
│  │ - 价格管理│  │ - 物流   │  │ - 等级   │  │ - 秒杀   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
│  │客户服务   │  │数据分析   │  │财务结算  │               │
│  │ - 售后    │  │ - 报表   │  │ - 对账   │               │
│  │ - 投诉   │  │ - 推荐   │  │ - 发票   │               │
│  └──────────┘  └──────────┘  └──────────┘               │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

业务能力的三个特征:

  1. 对外可见——业务方/老板能用一句话讲清楚"这个能力提供什么价值"
  2. 相对稳定——5 年内不太可能消失或彻底重构(实现细节可以变)
  3. 完整闭环——能独立完成某项业务(不是"半成品")

举例对比:

名称 是业务能力吗? 原因
商品管理 ✅ 是 对外可见、稳定、完整
商品图片服务 ❌ 否 太细,是商品管理的实现细节
MyBatis 升级 ❌ 否 技术任务,不是业务能力
会员积分 ✅ 是 完整闭环(赚取、消耗、过期)
用户表 CRUD ❌ 否 数据操作,不是业务能力
营销活动 ✅ 是 对外可见、有产品经理

结论:业务能力地图是拆分服务的唯一第一标准。先画出能力地图,再考虑技术实现——实现可以变,能力的边界要稳。

# 4.2 能力边界识别

如何画出业务能力地图?三种主流方法:

方法 1 · 自顶向下分解

公司战略
   ↓
核心业务流程(如:用户下单 → 仓库发货 → 用户收货)
   ↓
关键活动(下单、支付、拣货、配送、签收)
   ↓
能力分组(交易履约 = 下单 + 支付;物流 = 拣货 + 配送 + 签收)
1
2
3
4
5
6
7

方法 2 · 自底向上聚合

所有现有功能(200 个 API)
   ↓
按"动什么数据"分组
   ↓
按"谁的需求驱动"分组
   ↓
合并成 8~15 个高内聚能力簇
1
2
3
4
5
6
7

方法 3 · 事件风暴(最推荐,第 9.1 节详述)

全团队拉一面墙
   ↓
橙色便利贴写"业务事件"(订单创建、支付成功、商品上架...)
   ↓
按事件流分组 → 自然形成上下文边界
1
2
3
4
5

能力边界的红绿灯标志:

标志 颜色 解读
同一名词在两处含义不同 🟢 绿灯,确认是边界 "用户"在订单里是 buyer,在评价里是 reviewer
同一操作在两处规则不同 🟢 绿灯,确认是边界 "审核"在内容侧 vs 在交易侧规则完全不同
跨越两处的修改要同步上线 🔴 红灯,可能切错了 改 A 必改 B → 它们其实是一个能力
跨越两处的查询 > 30% 接口 🔴 红灯,可能切错了 频繁 JOIN,本应在一起

# 4.3 高内聚低耦合标尺

服务划得好不好,用两把尺子量:

尺子 1 · 内聚度(Cohesion)——服务内部各部分有多紧密

类型 描述 评级
功能内聚 所有代码服务于同一业务能力 ⭐⭐⭐⭐⭐ 最佳
顺序内聚 A 的输出是 B 的输入(流水线) ⭐⭐⭐⭐ 良好
通信内聚 操作同一份数据 ⭐⭐⭐ 中等
时间内聚 在同一时机执行(如启动时) ⭐⭐ 较差
逻辑内聚 "都是工具函数" ⭐ 差
偶然内聚 "凑一起的" ❌ 最差

反面例子——"工具服务"是经典低内聚反模式:

❌ common-service:
   - 发短信
   - 生成订单号
   - 校验身份证
   - 调用天气接口
   - 计算两点距离
1
2
3
4
5
6

这些功能没有共同的业务能力,凑一起只因为"都很通用"——它会成为系统的垃圾桶,越长越大、越来越乱。

尺子 2 · 耦合度(Coupling)——服务之间的依赖程度

类型 描述 评级
事件耦合 A 发事件,B 订阅,A 不知道 B 存在 ⭐⭐⭐⭐⭐ 最佳
数据耦合 A 调 B 的 API,传递的是纯数据 ⭐⭐⭐⭐ 良好
控制耦合 A 调 B 时传"做什么"的标志位 ⭐⭐⭐ 中等
共享数据耦合 A、B 都读写同一张表 ⭐ 差
内容耦合 A 直接读 B 的内部状态 ❌ 最差

生产纪律:

  • 同上下文内:允许直接同步 RPC
  • 跨上下文:优先事件驱动,必要时 RPC(设熔断超时)
  • 任何情况都禁止:跨服务共享数据库表

# 4.4 反模式实体服务化

最常见、最害人的拆分反模式——按数据库表/实体一对一拆服务:

❌ 反模式:实体服务化
   - User 表 → 用户服务
   - Product 表 → 商品服务
   - Order 表 → 订单服务
   - Address 表 → 地址服务
   - Coupon 表 → 优惠券服务
   ...
1
2
3
4
5
6
7

问题暴露:

  1. 服务变成"远程 DAO"——每个服务就是"CRUD + 包一层 RPC",没有任何业务逻辑
  2. 业务逻辑无处安放——"下单时校验用户等级 + 计算优惠 + 扣库存"这种跨实体逻辑在哪?要么落在调用方(业务全在前台),要么落在某个服务里(强行依赖多个实体服务)
  3. 跨服务事务爆炸——任何业务操作都要协调 N 个实体服务,用 RPC 模拟 SQL JOIN
  4. 网状依赖——A 服务调 B 调 C 调 D,任何节点慢全链路慢

为什么这么多团队会踩?因为数据库模型 ≠ 业务能力——数据库是"信息的归类",业务能力是"价值的提供"。

正确做法——按业务能力聚合多个实体:

✅ 正确:按业务能力
   - 交易上下文(包含订单、支付单、履约单、地址等多张表)
   - 商品上下文(包含商品、库存、价格、规格等多张表)
   - 会员上下文(包含用户、等级、积分、收藏等多张表)
1
2
3
4

第 1 章案例的 18 个服务,至少 70% 都是实体服务化的产物——"地址服务""发票服务""价格服务"分别独立,本来都该归到"交易上下文"或"商品上下文"里。

# 5. 限界上下文拆分

# 5.1 通用语言的歧义

疑惑:"用户"这个词在系统里到处都是,要不要建一个"统一用户模型"?

论证:先看实际场景:

"用户" 在不同上下文里:
┌──────────────┬───────────────────────────────────────┐
│ 上下文        │ "用户" 是什么                          │
├──────────────┼───────────────────────────────────────┤
│ 登录认证      │ 凭证(account/password/token)           │
│ 个人中心      │ 个人资料(nickname/avatar/bio)          │
│ 下单履约      │ 买家(buyer_id/收货地址/支付方式)        │
│ 评价晒单      │ 评价者(reviewer_id/评级历史)            │
│ 营销活动      │ 营销对象(标签/分群/RFM)                 │
│ 客服工单      │ 投诉人(联系方式/历史投诉)               │
│ 财务对账      │ 应收对象(税号/发票抬头)                 │
└──────────────┴───────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

强行统一一个"用户模型"会怎样?

  • 字段越加越多:50 个字段的"全量用户对象"
  • 性能崩溃:取个昵称都要拉全量
  • 修改地狱:会员系统加个字段,所有调用方都受影响
  • 权限混乱:客服能看到的"用户"和买家自己看到的不该一样

结论:通用语言是上下文内部的统一,不是跨上下文的统一。"用户"在不同上下文里就是不同的东西——只是恰好都用了同一个词。

这就是 限界上下文(Bounded Context) 的核心命题——

在边界内,通用语言无歧义;跨越边界,必须显式翻译。

# 5.2 上下文边界识别

如何识别上下文边界?三个判定标准:

标准 1 · 同名异义

"订单" 在交易上下文 = 包含 items/payment/shipping 的复杂聚合
"订单" 在物流上下文 = 包含 weight/dimensions/route 的运单
→ 两个"订单"应在不同上下文
1
2
3

标准 2 · 规则差异

"商品下架" 在商品管理上下文 = 设置 status=offline
"商品下架" 在搜索上下文 = 从 ES 索引中移除
→ 两套规则的"下架"应在不同上下文
1
2
3

标准 3 · 团队归属

"会员等级" 由会员团队定义和维护
"VIP特权" 由营销团队定义和维护
→ 即使数据相关,也应在不同上下文
1
2
3

电商的典型上下文划分:

┌──────────────────────────────────────────────────────┐
│  核心子域 (Core Domain) ─ 公司竞争力所在               │
│  ┌──────────────┐  ┌──────────────┐                  │
│  │ 交易上下文     │  │ 商品上下文     │                  │
│  └──────────────┘  └──────────────┘                  │
├──────────────────────────────────────────────────────┤
│  支撑子域 (Supporting Subdomain) ─ 业务必需但非差异化   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐│
│  │ 会员上下文     │  │ 营销上下文     │  │ 物流上下文     ││
│  └──────────────┘  └──────────────┘  └──────────────┘│
├──────────────────────────────────────────────────────┤
│  通用子域 (Generic Subdomain) ─ 行业通用、可买可外包    │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐│
│  │ 认证上下文     │  │ 支付上下文     │  │ 通知上下文     ││
│  └──────────────┘  └──────────────┘  └──────────────┘│
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.3 上下文映射九模式

上下文之间不可能完全独立,必有协作关系。DDD 总结了 9 种上下文映射模式:

模式 描述 适用
共享内核(Shared Kernel) 两个上下文共享一小段代码/模型 上游强约定,慎用
客户-供应商(Customer-Supplier) 下游影响上游优先级 内部团队合作
追随者(Conformist) 下游完全适配上游,不抗争 上游是外部系统
防腐层(ACL, Anti-Corruption Layer) 下游建翻译层隔离上游 上游模型脏/不稳定 ★最常用
开放主机服务(OHS) 上游提供稳定的开放 API 多下游消费
发布语言(Published Language) 上下游约定标准协议(如 JSON Schema) 跨组织
分离方式(Separate Ways) 两个上下文不集成 集成成本 > 收益
大泥球(Big Ball of Mud) 边界混乱、互相耦合 ❌ 反模式
合作关系(Partnership) 双方平等紧密协作 战略级合作

最推荐:防腐层(ACL)——本上下文坚持自己的模型,对外部依赖建一层翻译适配。

本上下文(订单)             ACL                  外部上下文(旧用户系统)
┌──────────────┐         ┌──────┐               ┌──────────────────┐
│ Buyer        │ ◄────── │ 翻译  │ ◄──────────── │ legacy_user.dat   │
│   id         │         │ 转换  │               │   USR_NO          │
│   nickname   │         │ 适配  │               │   USR_NM_CN      │
│   level      │         └──────┘               │   GRD_LV          │
└──────────────┘                                 └──────────────────┘
   本侧模型简洁              一层防腐墙                外部模型奇葩
1
2
3
4
5
6
7
8

ACL 的红利:外部模型再烂,本上下文不被污染;外部系统改了,只改 ACL 一个文件。

# 5.4 子域分类与战略选择

不同子域应该采用不同的资源投入策略:

子域类型 描述 应该做什么 不应该做什么
核心域(Core) 公司差异化优势所在 重金自研,最强团队 外包、买现成
支撑域(Supporting) 业务需要但不差异化 适度自研,普通团队 过度投入
通用域(Generic) 行业通用 买/外包/开源 自研

典型例子:

电商公司:
  - 核心:交易引擎、商品搜索(自研,集中火力)
  - 支撑:会员、营销、物流(适度自研或买)
  - 通用:登录认证(用 OAuth2 标准)、邮件短信(用第三方)、支付(用支付宝/微信)

银行:
  - 核心:信贷模型、风控(重金自研)
  - 支撑:网银 UI、客户管理(适度自研)
  - 通用:登录、消息推送(买)
1
2
3
4
5
6
7
8
9

判断纪律:

  • 核心域识别错了,公司战略就错了——把通用域当核心域投入 = 浪费;把核心域外包 = 自废武功
  • 每年 review 一次——业务变化会让"核心"和"通用"互换
  • 同一份代码,对 A 公司是核心域,对 B 公司可能是支撑域

# 6. 服务粒度权衡

# 6.1 拆细的代价

疑惑:服务越细越好吗?反正机器便宜、容器轻量?

论证:拆得过细的真实代价,逐项列出:

代价 1 · 网络开销

单体内调用:~1 μs(函数调用)
进程内 RPC:~50 μs(序列化 + 反序列化)
同机房 RPC:~1 ms
跨机房 RPC:~10 ms
跨地域 RPC:~50 ms

一次业务操作 = N 个跨服务调用 × M 次往返
N=12, M=2 → 跨机房就是 240 ms 的网络墙
1
2
3
4
5
6
7
8

代价 2 · 故障组合爆炸

单体可用性 99.9%
10 个服务串行 = 0.999^10 = 99.0%(故障率涨 10 倍)
20 个服务串行 = 0.999^20 = 98.0%
1
2
3

代价 3 · 运维复杂度

每个服务都需要:

  • 独立的 CI/CD 流水线
  • 独立的监控面板
  • 独立的日志收集
  • 独立的告警规则
  • 独立的容量规划
  • 独立的灰度/回滚机制
  • 独立的 oncall 人员

18 个服务 = 18 套运维 = 至少 3 个专职 SRE

代价 4 · 调试地狱

一个 bug 可能跨 12 个服务、6 个团队、3 个时区——根因定位时间从 30 分钟涨到 3 天。

代价 5 · 团队隔阂

每个团队只懂自己的服务,没人懂全貌——架构演进、跨域优化、紧急救火都缺总指挥。

# 6.2 拆粗的代价

反过来,拆得太粗也有代价:

代价 1 · 单服务过大——又退化回小单体,所有单体之痛回来一遍

代价 2 · 发布粒度大——任何小改动要全服务发布,回归测试成本高

代价 3 · 团队冲突——多个团队挤在一个服务里,又开始抢 PR 优先级

代价 4 · 扩容浪费——一个服务里 80% 是低 QPS 功能,但要按高 QPS 部分扩容

代价 5 · 技术债务难偿——大服务积累的债务很难局部重构

# 6.3 粒度判断五维表

服务粒度的"恰到好处"用五维表来判断:

维度 拆细信号 拆粗信号
变化频率 A 周改 5 次、B 月改 1 次 → 拆 两部分总是一起改 → 合
QPS 量级 A 是 B 的 10 倍 → 拆 量级接近 → 合
团队归属 A、B 是不同团队的需求 → 拆 同一团队 → 合
数据耦合度 几乎不读对方数据 → 拆 70% 接口都要 JOIN → 合
业务规则差异 A、B 完全不同业务语义 → 拆 规则强耦合 → 合

五维全打 ✓ 才拆,否则不拆。

典型决策示例:

"价格"和"优惠"该不该拆?
- 变化频率:营销活动经常改优惠规则,价格相对稳定 → ✓
- QPS:价格查询是优惠查询的 5 倍 → ✓
- 团队:定价团队 vs 营销团队 → ✓
- 数据耦合:算最终价时 95% 接口要同时调两边 → ✗
- 业务规则:本质都是"算出最终价格" → ✗

→ 五维 3 ✓ 2 ✗,应该合并到一个"定价上下文",
  内部区分基础价模块和优惠模块
1
2
3
4
5
6
7
8
9

第 1 章案例中"价格服务"和"优惠服务"分开,就是只看了前 3 项,忽略了后 2 项。

# 6.4 单体优先原则

Martin Fowler 在 2015 年提出 "Monolith First" 原则:

几乎所有成功的微服务系统,都是从一个成功的单体演化出来的;几乎所有从零开始的微服务系统,最后都失败了。

为什么单体优先?

  1. 业务模型未稳定——新业务的边界你根本不知道在哪,拆得越早越拆错
  2. 团队规模不够——15 个人维护 10 个服务,平均每服务 1.5 人,必然崩塌
  3. 基础设施缺失——没有完善的监控、链路追踪、灰度、容器编排,微服务等于裸奔
  4. 重构成本不对称——单体内部重构边界很便宜,跨服务调整边界要数据迁移 + API 兼容 + 多方协调

"先单体后微服务"的工程纪律:

启动阶段(0~1 年):纯单体
   ├─ 业务跑通最重要
   └─ 把边界画在代码里(package/module),不画在进程里

成长阶段(1~3 年):模块化单体
   ├─ 严格的内部模块边界
   ├─ 模块间通过接口调用
   ├─ 数据按模块分 schema
   └─ "假装"已经是微服务,但只有一个进程

爆发阶段(3+ 年):选择性拆出
   ├─ 哪个模块最痛(发布频率高/扩容压力大)→ 优先拆它
   ├─ 一次拆一个,稳定运行 3 个月再拆下一个
   └─ 永远不要"一次性微服务化"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

反例提醒:第 1 章那家公司,业务才 1 年、团队 30 人,就一次性拆出 18 个服务——踩了"单体优先"原则的每一个反面。

# 7. 数据库拆分策略

# 7.1 共享数据库反模式

疑惑:服务拆开了,数据库共用一个不行吗?拆库多麻烦?

论证:试试看:

❌ 共享数据库
┌─────────┐  ┌─────────┐  ┌─────────┐
│订单服务  │  │商品服务  │  │用户服务  │
└────┬────┘  └────┬────┘  └────┬────┘
     │            │            │
     └────────────┼────────────┘
                  ▼
            ┌──────────┐
            │ 同一个 DB │  ← 80 张表,所有服务直接读写
            └──────────┘
1
2
3
4
5
6
7
8
9
10

问题暴露:

  1. 表结构变更需要全服务对齐——加个字段要协调所有服务发布,回到单体的发布噩梦
  2. 服务边界形同虚设——你说"商品服务管商品表",但订单服务直接 SELECT * FROM products 一行 SQL 解决,服务隔离被绕过
  3. 数据库性能瓶颈——所有服务争抢同一个 DB 连接池,慢查询互相拖累
  4. 故障域不收敛——DB 一挂,所有服务都挂
  5. 无法独立扩容——商品库需要分片,订单库需要主从,但共享时只能按最复杂的来
  6. 数据契约模糊——表 = 服务间的隐式契约,但表结构变动没人通知,最隐蔽的耦合

结论:共享数据库 = 假微服务真单体。这是微服务化最危险的反模式,没有之一。

# 7.2 一库一服务

正确的范式——Database per Service:

✅ 一库一服务
┌─────────┐    ┌─────────┐    ┌─────────┐
│订单服务  │    │商品服务  │    │用户服务  │
└────┬────┘    └────┬────┘    └────┬────┘
     ▼              ▼              ▼
  ┌──────┐      ┌──────┐      ┌──────┐
  │OrderDB│     │ProdDB│      │UserDB│
  └──────┘      └──────┘      └──────┘
   MySQL         MySQL+ES      MySQL
1
2
3
4
5
6
7
8
9

严格纪律:

  1. 物理隔离——每个服务有独立的 DB 实例(或至少独立 schema + 严格权限)
  2. 禁止跨服务直接读写表——只能通过对方的 API
  3. 独立选型——订单用 MySQL、商品用 MySQL+ES、用户用 PostgreSQL,按需选
  4. 独立扩容——分片、读写分离、备份策略各自决定

典型权限设置(MySQL):

-- 每个服务一个 user,只能访问自己的 db
CREATE USER 'order_svc'@'%' IDENTIFIED BY '...';
GRANT ALL ON order_db.* TO 'order_svc'@'%';
-- 显式禁止访问其他库
REVOKE ALL ON product_db.* FROM 'order_svc'@'%';
1
2
3
4
5

配套基础设施:

  • 每个服务 owner 团队管理自己的 DB(DBA 提供平台支持)
  • 数据备份、监控、慢查询各自独立
  • 跨库查询需求统一走数据仓库(离线)或事件订阅(实时)

# 7.3 跨库查询的解法

一库一服务后必然遇到——跨服务的查询怎么办?

场景:订单列表要展示"商品图片 + 商品名称",但商品数据在 ProductDB,订单数据在 OrderDB。

方案 A · API 编排(最简单,性能差)

public List<OrderListVO> queryOrders(...) {
    List<Order> orders = orderRepo.findByUser(userId);
    List<Long> productIds = orders.stream()
        .flatMap(o -> o.items().stream())
        .map(i -> i.productId())
        .collect(toList());

    Map<Long, Product> products = productClient.batchGet(productIds);

    return orders.stream()
        .map(o -> assembler.toVO(o, products))
        .collect(toList());
}
1
2
3
4
5
6
7
8
9
10
11
12
13

代价:每次列表都要远程调用商品服务;如果列表 100 单,批量 RPC 一次还行,但商品服务挂了列表就挂。

方案 B · 数据冗余(推荐,最实用)

订单创建时就把商品快照存进订单数据:

class OrderItem {
    Long productId;
    String productName;      // 冗余
    String productImage;     // 冗余
    BigDecimal price;        // 冗余(下单价格,不能变)
    Integer quantity;
}
1
2
3
4
5
6
7

红利:

  • 订单查询完全自给自足,不依赖商品服务
  • 商品改名/改价不影响历史订单(符合业务语义——下单时多少就是多少)

代价:

  • 商品信息变更时无法同步到订单(这通常正是想要的)
  • 存储成本略增

方案 C · CQRS + 物化视图(性能最优,复杂度高)

订单服务订阅商品服务的事件,本地维护一份"订单查询视图":

商品服务 → 商品变更事件 → Kafka → 订单服务 Projector → order_list_view(包含商品冗余)
1

详见 03.命令查询职责分离 第 5 章。

方案 D · 数据仓库(离线场景)

报表、BI、运营后台等弱实时场景,统一走数据仓库——所有库的数据通过 CDC 同步到 Hive/ClickHouse,跑大宽表 JOIN。

选型决策:

场景 推荐方案
C 端查询,要求 < 100ms 方案 B 冗余
C 端查询,多视图 方案 C CQRS
B 端管理后台 方案 A 编排
报表分析 方案 D 数仓

# 7.4 分布式事务的取舍

疑惑:单体里一个事务能搞定的事,拆分后怎么保证一致?

论证:先放弃幻想——分布式系统下没有真正的 ACID。CAP 定理告诉我们 C 和 A 在分区时必须二选一。

实际可选的方案:

方案 一致性 性能 复杂度 适用
本地事务+消息表(Outbox) 最终一致 高 中 ★ 大部分场景首选
TCC(Try-Confirm-Cancel) 强一致 中 高 资金、库存
Saga 编排 最终一致 高 中 长流程业务
XA 两阶段提交 强一致 极低 高 几乎不用
业务补偿 最终一致 高 低 失败可逆

详细对比和实现详见 04.事件驱动架构设计 第 6 章 Saga 编排、第 8 章三种投递语义。

生产纪律:

  1. 能用最终一致绝不用强一致——99% 业务场景能接受秒级最终一致
  2. 关键路径用 TCC 兜底——库存扣减、资金转账等不可逆操作
  3. 业务设计上避免分布式事务——重新审视边界,把"必须同时成功"的操作放到同一个上下文
  4. 设计可补偿性——任何跨服务操作都设计好"如何回滚"

# 8. 拆分中的陷阱

# 8.1 分布式单体

最普遍的拆分失败形态——Distributed Monolith(分布式单体):

表面:N 个独立服务
实质:必须一起发布、一起挂、一起扩容
1
2

判断信号:

信号 严重度
任何业务改动要 3 个以上服务同步发布 ⚠️⚠️⚠️
一个服务挂,整个业务不可用 ⚠️⚠️⚠️
服务间共享数据库或共享代码包 ⚠️⚠️⚠️
性能比单体差 10 倍以上 ⚠️⚠️
团队不能独立决策技术栈 ⚠️⚠️
监控只能看整体,无法定位单服务 ⚠️

根因:拆分时只动了进程边界,没动数据边界、团队边界、发布边界。

修复:

  • 强制一库一服务(数据边界)
  • 强制服务 owner 制(团队边界)
  • 强制独立 CI/CD(发布边界)
  • 引入事件驱动减少同步依赖

# 8.2 网状调用爆炸

服务多了之后必然出现的另一陷阱——调用关系网状化:

❌ 网状调用
   订单 ──→ 用户
    │  ╲   ╱  │
    │   ╲ ╱   │
    │    ╳    │
    │   ╱ ╲   │
    │  ╱   ╲  │
   商品 ──→ 库存
    │         │
    └→ 价格 ←─┘
       │
       ↓
      优惠

12 个服务,30+ 条依赖边
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

问题:

  • 任何拓扑变更要影响多方
  • 链路追踪图谱无法解读
  • 循环依赖随时出现
  • 谁也说不清"完整调用链"

应对——分层调用:

✅ 分层调用
┌──────────────────────────┐
│   编排层(BFF / 主流程)   │  ← 只它能调下层多个
└────┬────┬────┬────┬──────┘
     ▼    ▼    ▼    ▼
   订单  商品  会员  营销         ← 同层禁止互调
     │    │    │    │
     ▼    ▼    ▼    ▼
   订单库 商品库 ...             ← 各自数据库

纪律:
- 同层服务禁止互相同步调用
- 必须互相通信只能走事件总线(异步)
- 跨层调用单向(上调下,禁止下调上)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.3 共享模型回头路

拆完之后,开发者会本能地想——"既然两个服务都要 User,抽个 common-user-model 包不就好了?"

❌ 共享模型 jar 包
┌──────────────────┐
│  common-models   │  ← 包含 User/Order/Product 等 DTO
└────────┬─────────┘
         │ 被依赖
   ┌─────┼─────┐
   ▼     ▼     ▼
 订单服务 商品服务 用户服务
1
2
3
4
5
6
7
8

问题:

  • 改 User 加一个字段,所有服务要升级 jar 重新发布
  • 共享 jar 成为隐式契约,变成跨服务的祖传代码
  • 不同服务对 User 的"理解"被强行统一,违反限界上下文原则

正确做法:

  • 每个服务自己定义自己需要的 DTO
  • 跨服务传递通过 API 契约(OpenAPI/Proto)
  • 即使两边字段一样,也不要共享类

唯一例外:

  • 协议规范(如 gRPC proto 文件)可以版本化共享
  • 但要严格做向后兼容

# 8.4 五大反模式集锦

实战中最常踩的坑汇总:

反模式 1 · 按数据库表拆服务

❌ User 表 → 用户服务,Order 表 → 订单服务
✅ 按业务能力聚合多表
1
2

反模式 2 · 服务调用变成远程 SQL JOIN

❌ 业务流程串 5 个 RPC 等价于一个 SQL JOIN
✅ 同上下文内合并;跨上下文用数据冗余
1
2

反模式 3 · 所有服务一起发布

❌ 改 1 行涉及 3 个仓库,要协调 3 个团队发版
✅ 每个服务独立向后兼容演进
1
2

反模式 4 · 同步调用链超过 3 层

❌ A → B → C → D → E(任何一环挂全挂)
✅ 关键操作 ≤ 2 层;超出走事件
1
2

反模式 5 · 没有 owner 的"无主服务"

❌ 拆分时没考虑团队归属,几个月后服务无人维护
✅ 拆服务前先确定 owner 团队,没人接不拆
1
2

# 9. 落地实战剖析

# 9.1 事件风暴工作坊

识别上下文最强方法——Event Storming(事件风暴),Alberto Brandolini 2013 年提出。

操作流程:

准备:
- 一面 4 米长的墙
- 5 种颜色便利贴:橙(事件)/蓝(命令)/黄(角色)/红(问题)/紫(策略)
- 全员到场:产品/开发/运维/客服

第 1 步:发散写事件(橙色)
  - 每人想到的"业务事件"都贴上去
  - 用过去式:"订单已创建""支付已成功""库存已扣减"
  - 30 分钟,墙上 100+ 张

第 2 步:时间轴排序
  - 从左到右按时间排列
  - 找出主要业务流程线

第 3 步:标出命令和角色
  - 蓝色:触发事件的命令("创建订单")
  - 黄色:执行命令的角色("买家""客服")

第 4 步:识别聚合
  - 命令-事件对的归属对象("订单""商品")

第 5 步:划分上下文
  - 把高内聚的聚合圈起来,形成上下文
  - 上下文边界 = 团队归属边界

第 6 步:标注关键问题
  - 红色:争议点、未明确处
  - 紫色:业务策略/规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

产出物:一张完整的"业务事件流图"——上下文边界自然浮现。

效果:一场 3 天的事件风暴,能产出过去6 个月架构会议都梳理不清的领域模型。

# 9.2 绞杀者迁移模式

老单体迁移到微服务的标准模式——Strangler Fig Pattern(绞杀者模式),Martin Fowler 命名:

阶段 1:原始单体
   ┌────────────────────┐
   │     Monolith       │
   │ (所有功能在一起)     │
   └────────────────────┘

阶段 2:在外面建网关
   ┌──────────────┐
   │   Gateway     │
   └──────┬───────┘
          ▼
   ┌────────────────────┐
   │     Monolith        │
   └────────────────────┘

阶段 3:抽出一个新服务
   ┌──────────────┐
   │   Gateway     │
   └──┬───────┬───┘
      ▼       ▼
   ┌──────┐ ┌────────────┐
   │新服务 │ │ Monolith    │
   └──────┘ └────────────┘
   ↑ 流量灰度切换:5% → 50% → 100%

阶段 4:继续抽,直到单体被"绞杀"
   ┌──────────────┐
   │   Gateway     │
   └──┬──┬──┬──┬──┘
      ▼  ▼  ▼  ▼
   服务 服务 服务 服务

阶段 5:单体下线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

关键纪律:

  1. 永远在外面套层网关——拆出来的服务和未拆的单体都通过网关,前端无感知
  2. 数据迁移最危险——新服务的数据库要从单体库里"摘"出来,常用 CDC 双写过渡
  3. 每次只拆一个上下文——稳定运行 3 个月再拆下一个
  4. 保留回退方案——灰度切流量,发现问题立刻退回单体路径

# 9.3 拆分顺序选择

老单体往外拆,先拆哪个?

优先拆出的特征:

维度 优先拆出
变更频率 高频改的模块(独立发布收益大)
性能压力 单独 QPS 高的模块(独立扩容)
团队冲突 多团队抢同一模块(独立归属)
业务边界 已经清晰稳定的(拆出风险低)
技术债务 想重写但拖了几年的(趁机重写)

留在最后拆的特征:

维度 留在后面
数据耦合 跟所有模块都有数据交集(账户、用户)
业务核心 公司命脉、风险承受度低
边界模糊 还在快速调整业务模型的

典型拆分顺序(电商):

单体 →
  第 1 拆:商品上下文(最先稳定)
       ↓ 3 个月
  第 2 拆:营销活动(独立性强、变更频繁)
       ↓ 3 个月
  第 3 拆:履约/物流(变更不大但独立性强)
       ↓ 3 个月
  第 4 拆:会员(数据耦合多,难度高)
       ↓ 3 个月
  最后:剩下交易主链路(核心,重大)
1
2
3
4
5
6
7
8
9
10

# 9.4 拆分成熟度评估

一个团队的微服务化是否成功,用 5 级成熟度模型评估:

等级 描述 信号
L0 单体 单一进程 < 30 人都该停在这
L1 模块化单体 单进程 + 强模块边界 50% 中型团队的理想态
L2 粗粒度服务 3~5 个核心上下文服务 真正微服务化的起点
L3 完整微服务 10~30 个服务 + 平台支撑 大厂常态
L4 中台架构 业务前台 + 能力中台 超大型组织

每级必备能力清单:

能力 L2 要求 L3 要求
CI/CD 每服务独立流水线 + 自动灰度
监控 服务级 RED 指标 + 全链路追踪
服务发现 DNS / 注册中心 + 健康检查熔断
配置中心 集中配置 + 灰度配置推送
容器编排 Docker Compose + Kubernetes
日志 集中采集 + 结构化 + 关联 traceId
安全 TLS + mTLS + 服务鉴权
文档 OpenAPI + 自动化契约测试

自我评估的硬指标:

  • 新人入职到能独立改一个服务 ≤ 2 周
  • P0 故障 RCA ≤ 4 小时
  • 任意服务独立发布零阻塞
  • 单服务挂掉不影响 70% 以上业务

任何一条不达标,说明拆得过早或运营能力不足。

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的 30 人团队 18 服务事故,七个疑问现在能逐条作答:

疑问 答案
① 什么时候必须从单体拆? 第 3.1:代码 50 万行+、团队 30+、模块 20+、发布频率受限——任意 3 条以上才考虑;案例只有"团队 30 人"一条
② 按业务能力 vs 按数据实体? 第 4.4:必须按业务能力;案例的"用户服务""地址服务""价格服务""优惠服务"全是实体服务化反模式
③ 限界上下文怎么识别? 第 5.2:同名异义、规则差异、团队归属三标准;案例完全没做这一步
④ 服务粒度多大才合适? 第 6.3:五维表打分;案例几乎全维度都不该拆这么细
⑤ 数据库要不要也拆? 第 7.2:必须一库一服务;案例做到了但跨库查询全靠 RPC
⑥ 为什么"看似拆开"还是一损俱损? 第 8.1:典型分布式单体——同步链路 12 层,任何一环挂全挂
⑦ 已有单体怎么安全演进? 第 9.2:绞杀者模式,单体→模块化单体→粗粒度服务→细粒度;案例直接从单体跳到 L3

修复方案(按代价从小到大):

方案 A · 服务合并回归(推荐起步)

18 个服务 → 6 个上下文级服务:
   - 商品上下文(商品+库存+价格)
   - 交易上下文(订单+支付+发票+地址)
   - 营销上下文(优惠+活动+秒杀)
   - 会员上下文(用户+积分+等级)
   - 履约上下文(物流+通知+消息)
   - 客户上下文(评价+客服+售后)

代价:2 个月重构,需要数据迁移
收益:调用链 12 → 3,P99 1.2s → 200ms,发布频率从月级回到周级
1
2
3
4
5
6
7
8
9
10

方案 B · 引入事件驱动

所有"通知类"调用改为事件发布:
- 下单成功 → 发事件 → 营销/积分/通知 异步订阅
- 主链路 RPC 从 12 个降到 4 个核心

代价:1 个月改造,引入 Kafka
收益:故障域收敛,主链路可用性从 98% 提升到 99.9%
1
2
3
4
5
6

方案 C · 退回模块化单体(最激进)

直接合并回 1 个模块化单体:
- 内部按上下文分 module
- 单一数据库按 schema 分
- 单一 CI/CD

代价:3 个月重大重构,但代码逻辑变化小
收益:彻底脱离分布式单体之苦,性能回到 80ms,团队回到单一仓库协作
1
2
3
4
5
6
7

生产建议:方案 C 是上策——既然团队只有 30 人,根本就不该拆成微服务。承认错误回退是工程上的勇气,比"硬扛"更职业。

# 10.2 一次拆分的全过程

把"一个上下文从单体里拆出来"的全过程串成一棵知识树:

决策"要拆订单上下文"
        │
        ├─ 评估阶段
        │   ├─ 单体成熟度评估(第 9.4 节)—— 团队规模、基础设施
        │   ├─ 业务边界判定(第 4 章)—— 是不是业务能力
        │   ├─ 上下文识别(第 5 章)—— 同名异义检查
        │   ├─ 粒度评估(第 6.3 节)—— 五维表打分
        │   └─ 优先级判断(第 9.3 节)—— 为什么先拆它
        │
        ├─ 设计阶段
        │   ├─ 事件风暴(第 9.1 节)—— 全员对齐业务模型
        │   ├─ 子域分类(第 5.4 节)—— 核心 / 支撑 / 通用
        │   ├─ 上下文映射(第 5.3 节)—— 与其他上下文的关系
        │   ├─ 数据库设计(第 7.2 节)—— 独立 schema 拆出
        │   └─ API 契约(OpenAPI / Proto)
        │
        ├─ 实施阶段
        │   ├─ 绞杀者网关(第 9.2 节)—— 流量入口前置
        │   ├─ 抽出代码 + 独立仓库
        │   ├─ 抽出数据 —— CDC 双写过渡
        │   ├─ 灰度切流 5% → 50% → 100%
        │   ├─ 旧路径下线
        │   └─ 单体瘦身
        │
        ├─ 运维阶段
        │   ├─ 独立 CI/CD 流水线
        │   ├─ 独立监控告警(RED 指标)
        │   ├─ 链路追踪接入
        │   ├─ 容量规划与扩容
        │   └─ Oncall 责任明确
        │
        └─ 反馈阶段
            ├─ 3 个月后复盘:拆得对吗?
            ├─ 性能/可用性是否改善?
            ├─ 团队效率是否提升?
            ├─ 必要时合回去(第 8.4 节五大反模式)
            └─ 下一个拆分对象(回到决策阶段)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

理解一次拆分的完整路径,就是理解为什么微服务化是一个长期工程,不是一次性技术决策。每一次拆分都应该有清晰的动因、设计、回退方案。

# 10.3 设计哲学回扣

整理本篇的四条跨架构适用的设计哲学:

哲学 1:边界即责任——拆分的本质是"谁负责什么"

服务边界不只是代码边界,更是团队归属边界、数据所有权边界、变更责任边界。一个服务必须有明确的 owner 团队,团队必须能完整决策"代码怎么写、数据怎么存、何时发布"。没有这三条,拆出来的服务就是"无主之地"——出问题踢皮球,加需求互相推。康威定律告诉我们,组织是因,系统是果。

哲学 2:业务能力大于技术实体——别让数据库表骗了你

"用户表"不是业务能力,"会员服务"才是;"订单表"不是业务能力,"交易上下文"才是。实体只是业务能力的载体,不是能力本身。按实体拆服务的结果就是把每个服务变成"远程 DAO"——业务逻辑无处安放,跨服务事务遍地,最后退化成一堆调来调去的网状结构。这条哲学是 DDD 的核心——先建模业务能力,再决定数据如何分布。

哲学 3:单体优先——拆分是手段不是目的

微服务不是越多越好。"先把事做成,再考虑拆"是工程铁律——业务还没跑通就拆服务,是把不确定性指数级放大。Martin Fowler 的"Monolith First"原则被无数项目验证:先单体跑成熟,再选择性拆出最痛的部分。一上来就微服务的项目,80% 失败。拆是为了解决问题,不是为了赶时髦。

哲学 4:渐进胜过革命——每一步都要可回退

服务拆分不是"砍一刀",是"绞杀者"——慢慢替换、灰度切流、保留回退。任何一次大规模重写都是高风险事件——业务逻辑遗漏、数据迁移出错、性能不达预期,任何一个都可能让项目崩盘。保持"可回退"是工程上的安全网——发现拆错了立刻合回去,比硬撑半年再合代价小一千倍。架构演进的速度上限,是团队消化复杂度的速度。

# 10.4 拆分决策速查

一张图保存以备查:

维度 单体 模块化单体 微服务(L3)
团队规模 < 30 人 30~80 人 80+ 人
代码量 < 20 万行 20~50 万行 50 万+
服务数 1 1(多 module) 10~30
数据库 共享 共享但分 schema 一库一服务
发布粒度 整体 整体 服务级
部署 物理机/VM Docker Kubernetes
监控 应用日志 + APM + 全链路追踪
一致性 ACID ACID 最终一致 + Saga
适用场景 MVP / 中小业务 中型 SaaS 大型多业务线

演进决策树:

                  系统起步
                     │
                     ▼
               团队 < 30 人?
                /            \
              是              否
              ↓               ↓
          单体起步         团队 < 80 人?
                          /            \
                        是              否
                        ↓               ↓
                  模块化单体        基础设施齐备?
                                   /         \
                                 是            否
                                 ↓             ↓
                              拆 3~5 个      先建平台
                              核心服务      再考虑拆
                                ↓
                            稳定 1 年后
                                ↓
                            继续拆细
                            (L3 微服务)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

60 秒诊断命令清单:

# 看代码规模
find . -name "*.java" | xargs wc -l | tail -1

# 看 git 提交频率(最近 30 天)
git log --since="30 days ago" --pretty=oneline | wc -l

# 看服务数量
kubectl get svc -n production | wc -l

# 看调用链层数(依赖链路追踪)
curl skywalking.../trace/$traceId | jq '.spans | length'

# 看跨服务调用 P99
curl prom.../api/v1/query?query=histogram_quantile(0.99,rpc_seconds)

# 看故障关联度(单服务挂时影响范围)
grep "5xx" gateway.log | awk '{print $upstream}' | sort | uniq -c

# 看团队-服务映射
cat services-ownership.yaml | yq '.services | length'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

拆分黄金法则:

拆分前提:    团队规模 > 30 + 模块边界稳定 + 基础设施齐备
拆分依据:    业务能力(不是数据表,不是部门)
拆分边界:    限界上下文(同名异义、规则差异、团队归属)
拆分粒度:    五维全 ✓ 才拆,宁粗勿细
拆分顺序:    高频改的先拆,核心交易最后拆
拆分方法:    绞杀者模式,灰度替换,永不大爆炸
数据策略:    一库一服务,禁止共享数据库
通信策略:    同上下文 RPC,跨上下文事件优先
一致性:      最终一致兜底,Saga 补偿关键路径
回退预案:    任何拆分都要能合回去
1
2
3
4
5
6
7
8
9
10

第 1 章案例:30 人团队 18 个服务 → 6 个上下文服务 / 模块化单体 → 调用链 12→3,P99 1.2s→200ms,发布从月级回到周级。这就是"按业务能力拆 + 单体优先"给团队的核心红利。


下一篇:我们已经知道了"服务边界如何划分",下一步进入 06.领域驱动战略设计——把"通用语言、限界上下文、上下文映射"从拆分实践提升到战略方法论级别。

#架构#微服务
上次更新: 2026/06/17, 11:43:57
事件驱动架构设计
领域驱动战略设计

← 事件驱动架构设计 领域驱动战略设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式