微服务拆分策略
# 微服务拆分策略
# 目录介绍
# 1. 案例引入
# 1.1 一拆就崩在哪
先看一段在真实公司发生过的"微服务化失败"——一家电商团队 30 人,原本是一个 Spring Boot 单体,QPS 5000,运转良好。CTO 听了几场架构大会后拍板"微服务化",三个月后拆出 18 个服务:
用户服务 商品服务 库存服务 价格服务 优惠服务
订单服务 支付服务 物流服务 地址服务 发票服务
评价服务 搜索服务 推荐服务 消息服务 通知服务
会员服务 积分服务 报表服务
2
3
4
拆完之后的现象:
- 单接口 P99 延迟:从 80ms 涨到 1.2s
- 上线发布:从每周 5 次降到每月 2 次(每次要协调 6+ 个团队)
- 故障率:从月均 1 次涨到周均 3 次(任何一个服务挂,下单全链路崩)
- 团队人效:新人上手时间从 2 周变成 2 个月(要看完 18 个服务才能改一行代码)
最荒诞的一幕:一个"用户下单"接口的调用链路:
API网关 → 订单服务 → 用户服务(拿用户信息)
→ 商品服务(拿商品信息)→ 价格服务(算价格)→ 优惠服务(算优惠)
→ 库存服务(扣库存)
→ 地址服务(拿收货地址)
→ 支付服务(创建支付单)
→ 消息服务(发下单消息)→ 通知服务(推 push)
→ 积分服务(预扣积分)
一次下单:12 个跨进程 RPC + 8 次跨库事务
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 章
2
3
4
5
6
7
# 1.3 我们要回答什么
这个事故就是本篇的主线案例。我们带着上面 7 个问号往下走,每讲完一段原理就解开一两个;最后在第 10 章把案例彻底剖开,并给出三种修复方案与各自的代价。
本篇路线:
架构总图 (第 2 章)
↓
单体到微服务 (第 3 章) ─→ 解开"什么时候该拆"
↓
按业务能力 → 限界上下文 (第 4-5 章) ─→ 解开"按什么标准拆"
↓
粒度权衡 → 数据库拆分 (第 6-7 章) ─→ 解开"拆到什么程度"
↓
陷阱集锦 (第 8 章) ─→ 解开"为什么拆完反而更差"
↓
落地实战 (第 9 章) ─→ 武器库
↓
综合案例 (第 10 章) ─→ 案例彻底剖开
2
3
4
5
6
7
8
9
10
11
12
13
📌 本篇定位:这是整个系列的战略决策篇。前面 02-04 篇讲"架构内部如何组织",本篇讲"系统外部如何切分"。读完本篇后,再面对任何"要不要拆服务"的争论,都能立刻回答:"为什么要拆、拆到什么程度、代价是什么"。
# 2. 架构概览
# 2.1 微服务体系总图
我们看一个成熟的微服务体系的全貌(以电商域为例):
┌──────────────────────┐
│ API 网关 │ ← 鉴权、限流、路由
└──────────┬───────────┘
▼
┌───────────────────────────────────────┐
│ BFF 层(按端聚合) │ ← Web BFF / App BFF
└───┬─────────────┬─────────────┬───────┘
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 订单上下文 │ │ 商品上下文 │ │ 会员上下文 │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │ 下单 │ │ │ │ 商品 │ │ │ │ 用户 │ │
│ │ 履约 │ │ │ │ 库存 │ │ │ │ 积分 │ │
│ │ 售后 │ │ │ │ 价格 │ │ │ │ 等级 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ 独占数据库 │ │ 独占数据库 │ │ 独占数据库 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────┼────────────────┘
▼
┌────────────────┐
│ 事件总线 │ ← Kafka 异步解耦
└────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 支付上下文 │ │ 物流上下文 │ │ 营销上下文 │
└──────────┘ └──────────┘ └──────────┘
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 为什么必须拆
为什么要把单体拆成微服务?而不是继续加机器、加缓存?
疑惑:单体性能不够就横向扩容,简单粗暴有效——为什么要付出微服务的复杂度代价?
论证:
- 代码规模超过认知极限——50 万行代码以上的单体,没有人能完整理解全貌。新人改一行可能挂掉 5 个不相关的功能。拆分把"全局复杂度"切成多个"局部复杂度"。
- 发布节奏被锁死——单体只有一个 CI/CD 流水线,任何一个模块要发版,整个系统要回归。100 人团队挤一个发布窗口,月发布次数 = 1。拆分让各团队独立发布。
- 技术栈被锁死——单体只能选一套技术栈。如果某个模块更适合 Go、某个模块需要 Python ML 库,单体里没法选。拆分让每个服务独立选型。
- 故障域无法隔离——单体里任何一个 NPE、死锁、内存泄漏都会拖垮整个进程。爆炸半径 = 整个业务。拆分让故障局部化。
- 扩容粒度不匹配——商品服务 QPS 10 万,订单服务 QPS 1 万——单体里两者绑死,只能按最高 QPS 扩容,资源浪费 10 倍。拆分让按需扩容。
- 团队规模超过 Two-Pizza——单一代码库超过 10 人协作,merge 冲突、需求排期、技术决策全要全员对齐——沟通成本以 N² 增长。拆分让团队独立决策。
- 反向验证:如果不拆会怎样?看看那些 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 协议臃肿,性能堪忧。
微服务的核心革命:
- 去掉中心化总线——服务直接互调,最多有个轻量服务发现
- 强制数据隔离——一库一服务,杜绝"通过共享数据库耦合"
- 完全自治团队——技术栈、发布节奏、运维方式各团队说了算
- 协议轻量化——HTTP/JSON 或 gRPC,没有 SOAP 那种历史包袱
结论:微服务不是 SOA 的升级,是对 SOA 中心化失败的反思后的重新设计。理解这一点,才能避免在落地时把微服务又做成 SOA(典型表现:堆一堆服务网格、ESB-like 中间件,又回到中心化)。
# 3.3 康威定律的反作用
1968 年 Melvin Conway 提出:
任何设计系统的组织,最终产生的设计等同于组织的沟通结构。
这是康威定律——系统结构会自动收敛到团队结构。
正向应用(设计组织 → 系统形态):
团队结构 系统形态
┌──────────┐ ┌──────────┐
│ 订单团队 │ │ 订单服务 │
└──────────┘ └──────────┘
┌──────────┐ ┌──────────┐
│ 商品团队 │ →(必然导致)→ │ 商品服务 │
└──────────┘ └──────────┘
┌──────────┐ ┌──────────┐
│ 支付团队 │ │ 支付服务 │
└──────────┘ └──────────┘
2
3
4
5
6
7
8
9
10
反向应用——逆康威(设计系统形态 → 调整组织):
想要某种系统架构?先把组织调成那种结构。
例子:要拆出"会员上下文"和"营销上下文"两个独立服务,先把原"会员营销组"拆成两个独立组,各自有 owner、各自承担 KPI——系统会自然按组织边界拆开。
反作用——本篇第 1 章案例最深的教训:30 人团队硬拆 18 个服务,违反康威定律——
组织:30 人,约 5 个小组
系统:18 个服务
每个服务平均 1.6 人维护——根本不够
→ 服务实际归属混乱,没人对哪个服务真正负责
→ 出问题踢皮球,加需求互相推
→ 系统逐渐腐化成"无主之地"
2
3
4
5
6
7
康威定律的工程纪律:
- 服务数量上限 ≈ 团队数量——一个团队最多维护 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:领域平台 + 业务前台
├─ 抽出共性能力(用户、订单、支付)作为中台
├─ 上层快速搭建业务前台
└─ 适用:超大型组织、多业务线
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)。业务能力是"组织提供的、对外可见的、稳定的价值单元"。
电商域典型业务能力地图:
┌─────────────────────────────────────────────────────────┐
│ 电商业务能力 │
├─────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │商品管理 │ │交易履约 │ │会员服务 │ │营销活动 │ │
│ │ - 商品发布│ │ - 下单 │ │ - 注册 │ │ - 优惠券 │ │
│ │ - 库存管理│ │ - 支付 │ │ - 登录 │ │ - 满减 │ │
│ │ - 价格管理│ │ - 物流 │ │ - 等级 │ │ - 秒杀 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │客户服务 │ │数据分析 │ │财务结算 │ │
│ │ - 售后 │ │ - 报表 │ │ - 对账 │ │
│ │ - 投诉 │ │ - 推荐 │ │ - 发票 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
业务能力的三个特征:
- 对外可见——业务方/老板能用一句话讲清楚"这个能力提供什么价值"
- 相对稳定——5 年内不太可能消失或彻底重构(实现细节可以变)
- 完整闭环——能独立完成某项业务(不是"半成品")
举例对比:
| 名称 | 是业务能力吗? | 原因 |
|---|---|---|
| 商品管理 | ✅ 是 | 对外可见、稳定、完整 |
| 商品图片服务 | ❌ 否 | 太细,是商品管理的实现细节 |
| MyBatis 升级 | ❌ 否 | 技术任务,不是业务能力 |
| 会员积分 | ✅ 是 | 完整闭环(赚取、消耗、过期) |
| 用户表 CRUD | ❌ 否 | 数据操作,不是业务能力 |
| 营销活动 | ✅ 是 | 对外可见、有产品经理 |
结论:业务能力地图是拆分服务的唯一第一标准。先画出能力地图,再考虑技术实现——实现可以变,能力的边界要稳。
# 4.2 能力边界识别
如何画出业务能力地图?三种主流方法:
方法 1 · 自顶向下分解
公司战略
↓
核心业务流程(如:用户下单 → 仓库发货 → 用户收货)
↓
关键活动(下单、支付、拣货、配送、签收)
↓
能力分组(交易履约 = 下单 + 支付;物流 = 拣货 + 配送 + 签收)
2
3
4
5
6
7
方法 2 · 自底向上聚合
所有现有功能(200 个 API)
↓
按"动什么数据"分组
↓
按"谁的需求驱动"分组
↓
合并成 8~15 个高内聚能力簇
2
3
4
5
6
7
方法 3 · 事件风暴(最推荐,第 9.1 节详述)
全团队拉一面墙
↓
橙色便利贴写"业务事件"(订单创建、支付成功、商品上架...)
↓
按事件流分组 → 自然形成上下文边界
2
3
4
5
能力边界的红绿灯标志:
| 标志 | 颜色 | 解读 |
|---|---|---|
| 同一名词在两处含义不同 | 🟢 绿灯,确认是边界 | "用户"在订单里是 buyer,在评价里是 reviewer |
| 同一操作在两处规则不同 | 🟢 绿灯,确认是边界 | "审核"在内容侧 vs 在交易侧规则完全不同 |
| 跨越两处的修改要同步上线 | 🔴 红灯,可能切错了 | 改 A 必改 B → 它们其实是一个能力 |
| 跨越两处的查询 > 30% 接口 | 🔴 红灯,可能切错了 | 频繁 JOIN,本应在一起 |
# 4.3 高内聚低耦合标尺
服务划得好不好,用两把尺子量:
尺子 1 · 内聚度(Cohesion)——服务内部各部分有多紧密
| 类型 | 描述 | 评级 |
|---|---|---|
| 功能内聚 | 所有代码服务于同一业务能力 | ⭐⭐⭐⭐⭐ 最佳 |
| 顺序内聚 | A 的输出是 B 的输入(流水线) | ⭐⭐⭐⭐ 良好 |
| 通信内聚 | 操作同一份数据 | ⭐⭐⭐ 中等 |
| 时间内聚 | 在同一时机执行(如启动时) | ⭐⭐ 较差 |
| 逻辑内聚 | "都是工具函数" | ⭐ 差 |
| 偶然内聚 | "凑一起的" | ❌ 最差 |
反面例子——"工具服务"是经典低内聚反模式:
❌ common-service:
- 发短信
- 生成订单号
- 校验身份证
- 调用天气接口
- 计算两点距离
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 表 → 优惠券服务
...
2
3
4
5
6
7
问题暴露:
- 服务变成"远程 DAO"——每个服务就是"CRUD + 包一层 RPC",没有任何业务逻辑
- 业务逻辑无处安放——"下单时校验用户等级 + 计算优惠 + 扣库存"这种跨实体逻辑在哪?要么落在调用方(业务全在前台),要么落在某个服务里(强行依赖多个实体服务)
- 跨服务事务爆炸——任何业务操作都要协调 N 个实体服务,用 RPC 模拟 SQL JOIN
- 网状依赖——A 服务调 B 调 C 调 D,任何节点慢全链路慢
为什么这么多团队会踩?因为数据库模型 ≠ 业务能力——数据库是"信息的归类",业务能力是"价值的提供"。
正确做法——按业务能力聚合多个实体:
✅ 正确:按业务能力
- 交易上下文(包含订单、支付单、履约单、地址等多张表)
- 商品上下文(包含商品、库存、价格、规格等多张表)
- 会员上下文(包含用户、等级、积分、收藏等多张表)
2
3
4
第 1 章案例的 18 个服务,至少 70% 都是实体服务化的产物——"地址服务""发票服务""价格服务"分别独立,本来都该归到"交易上下文"或"商品上下文"里。
# 5. 限界上下文拆分
# 5.1 通用语言的歧义
疑惑:"用户"这个词在系统里到处都是,要不要建一个"统一用户模型"?
论证:先看实际场景:
"用户" 在不同上下文里:
┌──────────────┬───────────────────────────────────────┐
│ 上下文 │ "用户" 是什么 │
├──────────────┼───────────────────────────────────────┤
│ 登录认证 │ 凭证(account/password/token) │
│ 个人中心 │ 个人资料(nickname/avatar/bio) │
│ 下单履约 │ 买家(buyer_id/收货地址/支付方式) │
│ 评价晒单 │ 评价者(reviewer_id/评级历史) │
│ 营销活动 │ 营销对象(标签/分群/RFM) │
│ 客服工单 │ 投诉人(联系方式/历史投诉) │
│ 财务对账 │ 应收对象(税号/发票抬头) │
└──────────────┴───────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
强行统一一个"用户模型"会怎样?
- 字段越加越多:50 个字段的"全量用户对象"
- 性能崩溃:取个昵称都要拉全量
- 修改地狱:会员系统加个字段,所有调用方都受影响
- 权限混乱:客服能看到的"用户"和买家自己看到的不该一样
结论:通用语言是上下文内部的统一,不是跨上下文的统一。"用户"在不同上下文里就是不同的东西——只是恰好都用了同一个词。
这就是 限界上下文(Bounded Context) 的核心命题——
在边界内,通用语言无歧义;跨越边界,必须显式翻译。
# 5.2 上下文边界识别
如何识别上下文边界?三个判定标准:
标准 1 · 同名异义
"订单" 在交易上下文 = 包含 items/payment/shipping 的复杂聚合
"订单" 在物流上下文 = 包含 weight/dimensions/route 的运单
→ 两个"订单"应在不同上下文
2
3
标准 2 · 规则差异
"商品下架" 在商品管理上下文 = 设置 status=offline
"商品下架" 在搜索上下文 = 从 ES 索引中移除
→ 两套规则的"下架"应在不同上下文
2
3
标准 3 · 团队归属
"会员等级" 由会员团队定义和维护
"VIP特权" 由营销团队定义和维护
→ 即使数据相关,也应在不同上下文
2
3
电商的典型上下文划分:
┌──────────────────────────────────────────────────────┐
│ 核心子域 (Core Domain) ─ 公司竞争力所在 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 交易上下文 │ │ 商品上下文 │ │
│ └──────────────┘ └──────────────┘ │
├──────────────────────────────────────────────────────┤
│ 支撑子域 (Supporting Subdomain) ─ 业务必需但非差异化 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐│
│ │ 会员上下文 │ │ 营销上下文 │ │ 物流上下文 ││
│ └──────────────┘ └──────────────┘ └──────────────┘│
├──────────────────────────────────────────────────────┤
│ 通用子域 (Generic Subdomain) ─ 行业通用、可买可外包 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐│
│ │ 认证上下文 │ │ 支付上下文 │ │ 通知上下文 ││
│ └──────────────┘ └──────────────┘ └──────────────┘│
└──────────────────────────────────────────────────────┘
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 │
└──────────────┘ └──────────────────┘
本侧模型简洁 一层防腐墙 外部模型奇葩
2
3
4
5
6
7
8
ACL 的红利:外部模型再烂,本上下文不被污染;外部系统改了,只改 ACL 一个文件。
# 5.4 子域分类与战略选择
不同子域应该采用不同的资源投入策略:
| 子域类型 | 描述 | 应该做什么 | 不应该做什么 |
|---|---|---|---|
| 核心域(Core) | 公司差异化优势所在 | 重金自研,最强团队 | 外包、买现成 |
| 支撑域(Supporting) | 业务需要但不差异化 | 适度自研,普通团队 | 过度投入 |
| 通用域(Generic) | 行业通用 | 买/外包/开源 | 自研 |
典型例子:
电商公司:
- 核心:交易引擎、商品搜索(自研,集中火力)
- 支撑:会员、营销、物流(适度自研或买)
- 通用:登录认证(用 OAuth2 标准)、邮件短信(用第三方)、支付(用支付宝/微信)
银行:
- 核心:信贷模型、风控(重金自研)
- 支撑:网银 UI、客户管理(适度自研)
- 通用:登录、消息推送(买)
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 的网络墙
2
3
4
5
6
7
8
代价 2 · 故障组合爆炸
单体可用性 99.9%
10 个服务串行 = 0.999^10 = 99.0%(故障率涨 10 倍)
20 个服务串行 = 0.999^20 = 98.0%
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 ✗,应该合并到一个"定价上下文",
内部区分基础价模块和优惠模块
2
3
4
5
6
7
8
9
第 1 章案例中"价格服务"和"优惠服务"分开,就是只看了前 3 项,忽略了后 2 项。
# 6.4 单体优先原则
Martin Fowler 在 2015 年提出 "Monolith First" 原则:
几乎所有成功的微服务系统,都是从一个成功的单体演化出来的;几乎所有从零开始的微服务系统,最后都失败了。
为什么单体优先?
- 业务模型未稳定——新业务的边界你根本不知道在哪,拆得越早越拆错
- 团队规模不够——15 个人维护 10 个服务,平均每服务 1.5 人,必然崩塌
- 基础设施缺失——没有完善的监控、链路追踪、灰度、容器编排,微服务等于裸奔
- 重构成本不对称——单体内部重构边界很便宜,跨服务调整边界要数据迁移 + API 兼容 + 多方协调
"先单体后微服务"的工程纪律:
启动阶段(0~1 年):纯单体
├─ 业务跑通最重要
└─ 把边界画在代码里(package/module),不画在进程里
成长阶段(1~3 年):模块化单体
├─ 严格的内部模块边界
├─ 模块间通过接口调用
├─ 数据按模块分 schema
└─ "假装"已经是微服务,但只有一个进程
爆发阶段(3+ 年):选择性拆出
├─ 哪个模块最痛(发布频率高/扩容压力大)→ 优先拆它
├─ 一次拆一个,稳定运行 3 个月再拆下一个
└─ 永远不要"一次性微服务化"
2
3
4
5
6
7
8
9
10
11
12
13
14
反例提醒:第 1 章那家公司,业务才 1 年、团队 30 人,就一次性拆出 18 个服务——踩了"单体优先"原则的每一个反面。
# 7. 数据库拆分策略
# 7.1 共享数据库反模式
疑惑:服务拆开了,数据库共用一个不行吗?拆库多麻烦?
论证:试试看:
❌ 共享数据库
┌─────────┐ ┌─────────┐ ┌─────────┐
│订单服务 │ │商品服务 │ │用户服务 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────┼────────────┘
▼
┌──────────┐
│ 同一个 DB │ ← 80 张表,所有服务直接读写
└──────────┘
2
3
4
5
6
7
8
9
10
问题暴露:
- 表结构变更需要全服务对齐——加个字段要协调所有服务发布,回到单体的发布噩梦
- 服务边界形同虚设——你说"商品服务管商品表",但订单服务直接
SELECT * FROM products一行 SQL 解决,服务隔离被绕过 - 数据库性能瓶颈——所有服务争抢同一个 DB 连接池,慢查询互相拖累
- 故障域不收敛——DB 一挂,所有服务都挂
- 无法独立扩容——商品库需要分片,订单库需要主从,但共享时只能按最复杂的来
- 数据契约模糊——表 = 服务间的隐式契约,但表结构变动没人通知,最隐蔽的耦合
结论:共享数据库 = 假微服务真单体。这是微服务化最危险的反模式,没有之一。
# 7.2 一库一服务
正确的范式——Database per Service:
✅ 一库一服务
┌─────────┐ ┌─────────┐ ┌─────────┐
│订单服务 │ │商品服务 │ │用户服务 │
└────┬────┘ └────┬────┘ └────┬────┘
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│OrderDB│ │ProdDB│ │UserDB│
└──────┘ └──────┘ └──────┘
MySQL MySQL+ES MySQL
2
3
4
5
6
7
8
9
严格纪律:
- 物理隔离——每个服务有独立的 DB 实例(或至少独立 schema + 严格权限)
- 禁止跨服务直接读写表——只能通过对方的 API
- 独立选型——订单用 MySQL、商品用 MySQL+ES、用户用 PostgreSQL,按需选
- 独立扩容——分片、读写分离、备份策略各自决定
典型权限设置(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'@'%';
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());
}
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;
}
2
3
4
5
6
7
红利:
- 订单查询完全自给自足,不依赖商品服务
- 商品改名/改价不影响历史订单(符合业务语义——下单时多少就是多少)
代价:
- 商品信息变更时无法同步到订单(这通常正是想要的)
- 存储成本略增
方案 C · CQRS + 物化视图(性能最优,复杂度高)
订单服务订阅商品服务的事件,本地维护一份"订单查询视图":
商品服务 → 商品变更事件 → Kafka → 订单服务 Projector → order_list_view(包含商品冗余)
详见 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 章三种投递语义。
生产纪律:
- 能用最终一致绝不用强一致——99% 业务场景能接受秒级最终一致
- 关键路径用 TCC 兜底——库存扣减、资金转账等不可逆操作
- 业务设计上避免分布式事务——重新审视边界,把"必须同时成功"的操作放到同一个上下文
- 设计可补偿性——任何跨服务操作都设计好"如何回滚"
# 8. 拆分中的陷阱
# 8.1 分布式单体
最普遍的拆分失败形态——Distributed Monolith(分布式单体):
表面:N 个独立服务
实质:必须一起发布、一起挂、一起扩容
2
判断信号:
| 信号 | 严重度 |
|---|---|
| 任何业务改动要 3 个以上服务同步发布 | ⚠️⚠️⚠️ |
| 一个服务挂,整个业务不可用 | ⚠️⚠️⚠️ |
| 服务间共享数据库或共享代码包 | ⚠️⚠️⚠️ |
| 性能比单体差 10 倍以上 | ⚠️⚠️ |
| 团队不能独立决策技术栈 | ⚠️⚠️ |
| 监控只能看整体,无法定位单服务 | ⚠️ |
根因:拆分时只动了进程边界,没动数据边界、团队边界、发布边界。
修复:
- 强制一库一服务(数据边界)
- 强制服务 owner 制(团队边界)
- 强制独立 CI/CD(发布边界)
- 引入事件驱动减少同步依赖
# 8.2 网状调用爆炸
服务多了之后必然出现的另一陷阱——调用关系网状化:
❌ 网状调用
订单 ──→ 用户
│ ╲ ╱ │
│ ╲ ╱ │
│ ╳ │
│ ╱ ╲ │
│ ╱ ╲ │
商品 ──→ 库存
│ │
└→ 价格 ←─┘
│
↓
优惠
12 个服务,30+ 条依赖边
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题:
- 任何拓扑变更要影响多方
- 链路追踪图谱无法解读
- 循环依赖随时出现
- 谁也说不清"完整调用链"
应对——分层调用:
✅ 分层调用
┌──────────────────────────┐
│ 编排层(BFF / 主流程) │ ← 只它能调下层多个
└────┬────┬────┬────┬──────┘
▼ ▼ ▼ ▼
订单 商品 会员 营销 ← 同层禁止互调
│ │ │ │
▼ ▼ ▼ ▼
订单库 商品库 ... ← 各自数据库
纪律:
- 同层服务禁止互相同步调用
- 必须互相通信只能走事件总线(异步)
- 跨层调用单向(上调下,禁止下调上)
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
└────────┬─────────┘
│ 被依赖
┌─────┼─────┐
▼ ▼ ▼
订单服务 商品服务 用户服务
2
3
4
5
6
7
8
问题:
- 改 User 加一个字段,所有服务要升级 jar 重新发布
- 共享 jar 成为隐式契约,变成跨服务的祖传代码
- 不同服务对 User 的"理解"被强行统一,违反限界上下文原则
正确做法:
- 每个服务自己定义自己需要的 DTO
- 跨服务传递通过 API 契约(OpenAPI/Proto)
- 即使两边字段一样,也不要共享类
唯一例外:
- 协议规范(如 gRPC proto 文件)可以版本化共享
- 但要严格做向后兼容
# 8.4 五大反模式集锦
实战中最常踩的坑汇总:
反模式 1 · 按数据库表拆服务
❌ User 表 → 用户服务,Order 表 → 订单服务
✅ 按业务能力聚合多表
2
反模式 2 · 服务调用变成远程 SQL JOIN
❌ 业务流程串 5 个 RPC 等价于一个 SQL JOIN
✅ 同上下文内合并;跨上下文用数据冗余
2
反模式 3 · 所有服务一起发布
❌ 改 1 行涉及 3 个仓库,要协调 3 个团队发版
✅ 每个服务独立向后兼容演进
2
反模式 4 · 同步调用链超过 3 层
❌ A → B → C → D → E(任何一环挂全挂)
✅ 关键操作 ≤ 2 层;超出走事件
2
反模式 5 · 没有 owner 的"无主服务"
❌ 拆分时没考虑团队归属,几个月后服务无人维护
✅ 拆服务前先确定 owner 团队,没人接不拆
2
# 9. 落地实战剖析
# 9.1 事件风暴工作坊
识别上下文最强方法——Event Storming(事件风暴),Alberto Brandolini 2013 年提出。
操作流程:
准备:
- 一面 4 米长的墙
- 5 种颜色便利贴:橙(事件)/蓝(命令)/黄(角色)/红(问题)/紫(策略)
- 全员到场:产品/开发/运维/客服
第 1 步:发散写事件(橙色)
- 每人想到的"业务事件"都贴上去
- 用过去式:"订单已创建""支付已成功""库存已扣减"
- 30 分钟,墙上 100+ 张
第 2 步:时间轴排序
- 从左到右按时间排列
- 找出主要业务流程线
第 3 步:标出命令和角色
- 蓝色:触发事件的命令("创建订单")
- 黄色:执行命令的角色("买家""客服")
第 4 步:识别聚合
- 命令-事件对的归属对象("订单""商品")
第 5 步:划分上下文
- 把高内聚的聚合圈起来,形成上下文
- 上下文边界 = 团队归属边界
第 6 步:标注关键问题
- 红色:争议点、未明确处
- 紫色:业务策略/规则
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:单体下线
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
关键纪律:
- 永远在外面套层网关——拆出来的服务和未拆的单体都通过网关,前端无感知
- 数据迁移最危险——新服务的数据库要从单体库里"摘"出来,常用 CDC 双写过渡
- 每次只拆一个上下文——稳定运行 3 个月再拆下一个
- 保留回退方案——灰度切流量,发现问题立刻退回单体路径
# 9.3 拆分顺序选择
老单体往外拆,先拆哪个?
优先拆出的特征:
| 维度 | 优先拆出 |
|---|---|
| 变更频率 | 高频改的模块(独立发布收益大) |
| 性能压力 | 单独 QPS 高的模块(独立扩容) |
| 团队冲突 | 多团队抢同一模块(独立归属) |
| 业务边界 | 已经清晰稳定的(拆出风险低) |
| 技术债务 | 想重写但拖了几年的(趁机重写) |
留在最后拆的特征:
| 维度 | 留在后面 |
|---|---|
| 数据耦合 | 跟所有模块都有数据交集(账户、用户) |
| 业务核心 | 公司命脉、风险承受度低 |
| 边界模糊 | 还在快速调整业务模型的 |
典型拆分顺序(电商):
单体 →
第 1 拆:商品上下文(最先稳定)
↓ 3 个月
第 2 拆:营销活动(独立性强、变更频繁)
↓ 3 个月
第 3 拆:履约/物流(变更不大但独立性强)
↓ 3 个月
第 4 拆:会员(数据耦合多,难度高)
↓ 3 个月
最后:剩下交易主链路(核心,重大)
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,发布频率从月级回到周级
2
3
4
5
6
7
8
9
10
方案 B · 引入事件驱动
所有"通知类"调用改为事件发布:
- 下单成功 → 发事件 → 营销/积分/通知 异步订阅
- 主链路 RPC 从 12 个降到 4 个核心
代价:1 个月改造,引入 Kafka
收益:故障域收敛,主链路可用性从 98% 提升到 99.9%
2
3
4
5
6
方案 C · 退回模块化单体(最激进)
直接合并回 1 个模块化单体:
- 内部按上下文分 module
- 单一数据库按 schema 分
- 单一 CI/CD
代价:3 个月重大重构,但代码逻辑变化小
收益:彻底脱离分布式单体之苦,性能回到 80ms,团队回到单一仓库协作
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 节五大反模式)
└─ 下一个拆分对象(回到决策阶段)
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 微服务)
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'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
拆分黄金法则:
拆分前提: 团队规模 > 30 + 模块边界稳定 + 基础设施齐备
拆分依据: 业务能力(不是数据表,不是部门)
拆分边界: 限界上下文(同名异义、规则差异、团队归属)
拆分粒度: 五维全 ✓ 才拆,宁粗勿细
拆分顺序: 高频改的先拆,核心交易最后拆
拆分方法: 绞杀者模式,灰度替换,永不大爆炸
数据策略: 一库一服务,禁止共享数据库
通信策略: 同上下文 RPC,跨上下文事件优先
一致性: 最终一致兜底,Saga 补偿关键路径
回退预案: 任何拆分都要能合回去
2
3
4
5
6
7
8
9
10
第 1 章案例:30 人团队 18 个服务 → 6 个上下文服务 / 模块化单体 → 调用链 12→3,P99 1.2s→200ms,发布从月级回到周级。这就是"按业务能力拆 + 单体优先"给团队的核心红利。
下一篇:我们已经知道了"服务边界如何划分",下一步进入 06.领域驱动战略设计——把"通用语言、限界上下文、上下文映射"从拆分实践提升到战略方法论级别。