架构演进实战指南
# 08.架构演进实战指南
# 目录介绍
# 1. 案例引入
# 1.1 一次失败的大爆炸重构
先看一段在某 50 人电商团队真实发生的"架构升级事故"——表面是技术升级,实际把整个团队拖入半年深渊:
背景:
- 团队规模: 50 人 (15 后端 + 10 前端 + 8 测试 + 其他)
- 系统: 5 年祖传 Spring Boot 单体, 80 万行代码, 单库 200 张表
- 痛点: 部署 40 分钟、改 A 模块挂 B 模块、性能瓶颈无法横向扩展
CTO 决策:
"学习业界最佳实践,一次性拆成 20 个微服务,3 个月完成"
执行:
Day 0: 启动会, 全员振奋
Day 30: 服务边界吵了一个月还没定 (按表拆? 按模块拆? 按团队拆?)
Day 60: 最终拆成 20 个服务, 开始编码迁移
Day 90: 80% 接口迁移完, 集成测试发现 200+ 跨服务调用
Day 120: 引入 Nacos + Sentinel + SkyWalking + Seata, 新增组件 5 个
Day 150: 灰度上线 5%, 链路追踪显示一个下单要 18 次 RPC, 延迟从 200ms 涨到 2.3s
Day 180: 决定回滚, 但数据库已分了,数据已双写,回不去
Day 210: 业务停滞、骨干离职 3 人、CTO 引咎辞职
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
直觉怀疑:是不是技术选型有问题?
查 GitHub:
- Nacos: 阿里出品, 业界标准 ✅ 没问题
- Sentinel: 限流降级, 业界标准 ✅ 没问题
- SkyWalking: APM 标杆 ✅ 没问题
- Seata: 分布式事务 ✅ 没问题
2
3
4
5
技术栈全对,团队全是中级以上工程师,却把项目搞砸了——这不是技术问题,是演进姿势的问题。
# 1.2 顺藤摸到根因
带着这条线往下挖:
- 假设 1:是不是拆得太多?—— 50 人团队拆 20 个服务,平均每个服务 2.5 人,连"两披萨原则"都凑不齐
- 假设 2:是不是拆得太急?—— 90 天从单体到 20 个微服务,没经过模块化、没经过数据库拆分,直接跳到终点
- 假设 3:是不是边界没想清楚?—— 200+ 跨服务调用 = 边界切错了,本该在一个服务内的业务被切散
- 假设 4:为什么回不去?—— 数据库已分,回滚需要数据合并 + 接口合并 + 部署合并,回滚成本 > 继续前进成本
- 假设 5:为什么"业界最佳实践"在我这反成毒药?—— 阿里几万人拆几千个服务是渐进 10 年的结果,不是 90 天一步到位
看似 "学最佳实践" 的决策,没毛病在选型,毛病在没有意识到架构是演进出来的不是设计出来的——这条决策碰到的不是 Spring 的坑,是架构演进方法论的坑。
这一段事故里至少藏着 8 个原理点:
① 什么时候该拆? 什么时候不该拆? → 第 3 章
② 单体一定不好吗? 模块化单体可以撑多久? → 第 4 章
③ 数据库不拆能跑微服务吗? → 第 5 章
④ 拆服务第一刀应该切哪里? → 第 6 章
⑤ 老系统怎么不停机迁移? → 第 7 章 (绞杀者)
⑥ 大爆炸重构错在哪? 渐进重构怎么做? → 第 8 章
⑦ 演进过程出问题怎么回滚? → 第 9 章
⑧ 怎么度量演进的成功与失败? → 第 9 章
2
3
4
5
6
7
8
# 1.3 我们要回答什么
这个事故就是本篇的主线案例。我们带着上面 8 个问号往下走,每讲完一段方法论就解开一两个;最后在第 10 章把案例彻底剖开,并给出"如果重新来一次该怎么做"的渐进式方案。
本篇路线:
演进总图 (第 2 章)
↓
单体起步 (第 3 章) ─→ 解开"为什么单体不丢人"
↓
模块化单体 → 数据库拆分 → 服务拆分 (第 4-6 章) ─→ 解开"三步走的演进路径"
↓
绞杀者 → 反模式 (第 7-8 章) ─→ 解开"老系统平滑迁移与四大病"
↓
度量与回滚 (第 9 章) ─→ 武器库
↓
综合案例 (第 10 章) ─→ 案例彻底剖开
2
3
4
5
6
7
8
9
10
11
📌 本篇定位:这是整个系列的实战收官篇。第 02-07 篇讲的六边形、CQRS、事件驱动、微服务、DDD、评审都是"招式",本篇把它们串成"组合拳"——回答"从单体到微服务,每一步具体怎么做"。读完本篇后,再看任何一个老系统升级的需求,都能立刻回答:"它现在在哪一阶段、下一步该走哪一步"。
# 2. 演进全景图
# 2.1 五阶段总图
我们把"单体到微服务"的完整演进路径切成五个阶段:
阶段 0 阶段 1 阶段 2
┌────────────┐ ┌────────────────┐ ┌──────────────┐
│ 单体起步 │──→ │ 模块化单体 │──→ │ 数据库拆分 │
│ (Monolith) │ │ (Modular Mono) │ │ (DB Split) │
└────────────┘ └────────────────┘ └──────────────┘
团队 < 10 团队 10~30 团队 30~50
表 < 100 表 100~300 表 300~500
单库单服务 单库多模块 多库多模块
│
▼
阶段 3 阶段 4
┌──────────────┐ ┌──────────────┐
│ 服务拆分 │──→ │ 微服务体系 │
│ (Svc Split) │ │ (Microsvc) │
└──────────────┘ └──────────────┘
团队 50~100 团队 > 100
服务 5~10 服务 10~50
多库少服务 多库多服务
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
五阶段的核心属性速查:
| 阶段 | 团队规模 | 代码行数 | 数据库 | 服务数 | 部署方式 |
|---|---|---|---|---|---|
| 0 单体 | < 10 | < 10 万 | 1 库 | 1 服务 | 单包部署 |
| 1 模块化单体 | 10~30 | 10~50 万 | 1 库 | 1 服务 | 单包部署 + 模块隔离 |
| 2 数据库拆分 | 30~50 | 50~100 万 | N 库 | 1~3 服务 | 少量服务 + 多库 |
| 3 服务拆分 | 50~100 | 100~300 万 | N 库 | 5~10 服务 | 中等微服务 |
| 4 微服务体系 | > 100 | > 300 万 | N 库 | 10~50 服务 | 完整微服务治理 |
每个阶段的核心动作:
阶段 0 → 1: 包结构重构 (按业务划分 module),保持单库单部署
阶段 1 → 2: 一库一模块, 跨库查询走应用层
阶段 2 → 3: 高频独立模块独立成服务, 进程内调用变 RPC
阶段 3 → 4: 引入服务网格、配置中心、链路追踪、限流降级
2
3
4
# 2.2 为什么这么切
为什么把演进切成"五阶段渐进",而不是"一步到位拆微服务"?
疑惑:第 1 章 CTO 是不是直接从阶段 0 跳到阶段 4,所以才挂的?
论证:
每阶段解决的问题不同——阶段 1 解决"代码乱"(边界),阶段 2 解决"数据耦合"(共享库),阶段 3 解决"部署耦合"(独立发布),阶段 4 解决"治理混乱"(服务网格)。跳阶段 = 同时解多个问题 = 没一个能解好。
每阶段验证学习不同——阶段 1 验证"边界是否合理",阶段 2 验证"跨库查询能否承受",阶段 3 验证"RPC 延迟与故障容忍"。跳阶段 = 没机会试错 = 错了无法局部修复。
每阶段成本递增——阶段 1 几乎 0 基础设施投入,阶段 4 要 Nacos/Sentinel/SkyWalking/Seata/K8s 全套。跳阶段 = 团队还没准备好就背巨额成本。
每阶段回滚代价不同——阶段 1 改包结构错了改回去 1 周,阶段 4 数据拆完了回不去半年。渐进式 = 每步可回滚 = 风险可控。
反向验证:如果不分阶段会怎样?参考第 1 章的 50 人团队——90 天 0 → 4,结果 210 天还在挣扎。分阶段不是慢,是真正的快。
结论:分阶段不是为了"形式上完整",而是把问题、验证、成本、回滚这四个独立维度同时编码进时间轴——每阶段稳定 6~12 个月再进下一阶段,让架构演进的风险始终可控、随时可回。这是"渐进式架构演进"的根基哲学。
下面我们从最基础的"单体起步"开始,看每一步具体怎么做。
# 3. 单体起步阶段
# 3.1 单体的合理性
疑惑:业界都在喊"微服务",新项目还能用单体吗?
论证:试试反过来想——一个 5 人创业团队,第一天就 20 个微服务,会怎样?
5 人团队 20 微服务:
- 每人维护 4 个服务,上下文切换累死
- 一次需求改 3 个服务,联调 2 周
- 跨服务调用 = 网络 = 故障概率指数级
- 基础设施(Nacos/K8s/链路追踪)5 人养不起
- MVP 还没出来,团队就崩了
5 人团队 1 个单体:
- 所有人共享上下文,改代码即时生效
- 一次需求一处改,本地起就能测
- 调用 = 函数 = 0 网络故障
- 一台机器 + Nginx 全搞定
- 2 周出 MVP,先活下来再说
2
3
4
5
6
7
8
9
10
11
12
13
结论:单体不是"落后",是"早期阶段的最优解"。Martin Fowler 提出的 Monolith First(单体优先)原则——没有验证过的边界,不要先拆。
单体的核心红利:
┌──────────────────────────────────────────────────────┐
│ 单体架构的四大红利 │
├──────────────────────────────────────────────────────┤
│ 1. 简单部署: 一个 jar/war,scp + restart 完事 │
│ 2. 简单调试: 本地 IDE 启动,断点直达任何函数 │
│ 3. 简单事务: @Transactional 一把锁,ACID 自动保证 │
│ 4. 简单观测: 一份日志,grep 即可 │
└──────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
适用判定:
当满足以下任一条件 → 单体优先:
☑ 团队 < 10 人
☑ 业务边界未稳定 (产品频繁变方向)
☑ MVP 阶段, 6 个月内可能砍方向
☑ 单机能扛得住 (QPS < 1000、数据 < 100 GB)
☑ 团队无微服务运维经验
不应该单体优先 → 直接上模块化甚至更高:
☐ 业务边界极其清晰 (如金融账户、电商下单)
☐ 团队规模 > 30 人, 必须并行开发
☐ 业务量已大 (QPS > 10000)
2
3
4
5
6
7
8
9
10
11
# 3.2 分层单体代码
最常见的单体结构是分层架构(参见 [01.分层架构设计详解]):
┌──────────────────────────────────────────────┐
│ shop-monolith (单体) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Controller 层 │ │
│ │ - OrderController │ │
│ │ - ProductController │ │
│ │ - UserController │ │
│ └─────────────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Service 层 │ │
│ │ - OrderService │ │
│ │ - ProductService │ │
│ │ - UserService │ │
│ └─────────────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Repository 层 (Mybatis/JPA) │ │
│ └─────────────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Database (单库 200 张表) │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
典型代码(极简版):
// 包结构 (按"技术分层"组织)
com.shop
├── controller
│ ├── OrderController.java
│ ├── ProductController.java
│ └── UserController.java
├── service
│ ├── OrderService.java
│ ├── ProductService.java
│ └── UserService.java
├── repository
│ ├── OrderRepository.java
│ ├── ProductRepository.java
│ └── UserRepository.java
└── entity
├── Order.java
├── Product.java
└── User.java
// 典型调用 (跨业务直接调)
@Service
public class OrderService {
@Autowired private ProductService productService; // 跨业务 ← 容易耦合
@Autowired private UserService userService; // 跨业务
@Transactional
public Order createOrder(Long userId, Long productId) {
User user = userService.getById(userId);
Product product = productService.getById(productId);
productService.deductStock(productId, 1); // 跨业务直接改库存
return orderRepository.save(new Order(user, product));
}
}
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
这就是阶段 0 的典型样子——按技术分层组织、业务模块通过 @Autowired 直接调用、共享同一个事务、共享同一个数据库。
# 3.3 单体的临界点
疑惑:单体什么时候该升级?
论证:观察四个信号——任何一个出现,就该考虑升级到模块化单体:
信号 1: 启动慢 (> 60 秒)
根因: 类太多、Spring 扫描慢、Bean 数 > 1000
信号 2: 部署慢 (> 20 分钟)
根因: 构建慢、测试慢、回滚慢
信号 3: 改 A 挂 B
根因: 模块边界模糊, 共用 Service 牵一发动全身
信号 4: 团队抢代码
根因: 5+ 个团队改同一个 Service.java, merge 冲突天天有
2
3
4
5
6
7
8
9
10
11
临界点判定表:
| 指标 | 健康 | 警戒 | 必须升级 |
|---|---|---|---|
| 代码行数 | < 10 万 | 10~30 万 | > 30 万 |
| 启动时间 | < 30s | 30~60s | > 60s |
| 部署时间 | < 5min | 5~20min | > 20min |
| 团队人数 | < 10 | 10~20 | > 20 |
| 跨模块改动占比 | < 20% | 20~40% | > 40% |
| 月度集成冲突 | < 10 次 | 10~30 次 | > 30 次 |
第 1 章案例的回顾:
原始单体的指标:
代码: 80 万行 ❌ 严重超标
启动: 3 分钟 ❌ 严重超标
部署: 40 分钟 ❌ 严重超标
团队: 15 后端 ⚠️ 临界
跨模块改动: 60% ❌ 严重超标
→ 早就到"必须升级"状态了
→ 但应升到"阶段 1 模块化单体",不是直接到"阶段 4 微服务"
2
3
4
5
6
7
8
9
# 3.4 演进前的准备
疑惑:升级前要做什么准备?
论证:演进 ≠ 直接动手——需要先做四件准备工作:
准备 1: 业务梳理 (1~2 周)
- 列出所有业务能力 (Capabilities)
- 识别核心域 / 支撑域 / 通用域 (参见 06.DDD)
- 找出业务边界冲突点
准备 2: 技术现状评估 (1 周)
- 代码扫描: 类 / 接口 / 包依赖关系
- 数据库扫描: 表 / 外键 / 跨业务 JOIN
- 调用链路: 哪些方法跨"业务模块"调用
准备 3: 团队能力评估 (1 周)
- 团队规模与组织结构 (康威定律)
- 微服务运维经验
- DevOps 基础设施现状
准备 4: 风险与回滚预案 (1 周)
- 演进各阶段的回滚成本
- 业务方对停机的容忍度
- 关键人是否到位
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
工具清单:
# 代码依赖扫描
jdeps -verbose:class shop.jar > deps.txt
# 数据库 JOIN 扫描 (找跨业务表的 JOIN)
grep -r "JOIN" src/ | grep -E "(order|product|user)"
# 包依赖可视化
sonarqube 或 IDEA Diagram
# 调用链路 (生产环境采样)
SkyWalking / Arthas trace
2
3
4
5
6
7
8
9
10
11
结论:不做准备就开始演进 = 第 1 章悲剧重演。准备的目的是回答两个问题——"现在在哪里"、"下一步去哪里"。任何一个答不清楚,就不要动手。
# 4. 模块化单体阶段
# 4.1 模块化的核心
疑惑:模块化单体和普通单体有什么区别?不就是改改包名吗?
论证:模块化单体是单体的进化形态——保留单体的部署简单,但通过强制边界解决"改 A 挂 B"。
核心思想:
普通单体: 按"技术分层"组织, 跨模块随意 import
模块化单体: 按"业务能力"组织, 模块间禁止跨边界 import
2
对比图:
普通单体 (按技术分层): 模块化单体 (按业务能力):
┌─────────────────────┐ ┌─────────────────────┐
│ controller │ │ order product user │
├─────────────────────┤ │ ┌─┐ ┌─┐ ┌─┐ │
│ service │ │ │c│ │c│ │c│ │
├─────────────────────┤ │ │s│ │s│ │s│ │
│ repository │ │ │r│ │r│ │r│ │
└─────────────────────┘ │ └─┘ └─┘ └─┘ │
└─────────────────────┘
跨模块直接调用 ← 边界模糊 模块间通过 API 调用 ← 边界清晰
2
3
4
5
6
7
8
9
10
模块化的四大纪律:
1. 模块内部包私有 (package-private)
- 默认 default 修饰符, 不允许外部 import
2. 模块对外只暴露 API 接口
- 通过 `api` 子包暴露, 其他都是 internal
3. 模块间调用走"模块边界 API"
- 不允许跨模块 import internal 类
4. 模块独立数据表
- 一个模块的表, 其他模块只能通过 API 读, 不能直接 SQL
2
3
4
5
6
7
8
9
10
11
# 4.2 包结构重构
重构前(按技术分层):
com.shop
├── controller ← 所有 Controller 混一起
├── service ← 所有 Service 混一起
├── repository ← 所有 Repository 混一起
└── entity ← 所有 Entity 混一起
2
3
4
5
重构后(按业务能力):
com.shop
├── order ← 订单模块
│ ├── api ← 对外暴露
│ │ ├── OrderApi.java ← 接口
│ │ └── OrderDto.java ← DTO
│ └── internal ← 内部实现 (package-private)
│ ├── OrderController.java
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── OrderEntity.java
│
├── product ← 商品模块
│ ├── api
│ │ ├── ProductApi.java
│ │ └── ProductDto.java
│ └── internal
│ ├── ProductController.java
│ ├── ProductService.java
│ └── ...
│
├── user ← 用户模块
│ ├── api
│ └── internal
│
└── common ← 公共基础设施
├── exception
├── utils
└── config
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
关键代码示范:
// 1. 模块对外 API (com.shop.product.api)
public interface ProductApi {
ProductDto getById(Long id);
void deductStock(Long id, int qty);
}
// 2. 模块内部实现 (com.shop.product.internal) - 包私有
@Service
class ProductServiceImpl implements ProductApi { // ← 注意:无 public
@Autowired private ProductRepository repository;
@Override
public ProductDto getById(Long id) {
ProductEntity entity = repository.findById(id).orElseThrow();
return new ProductDto(entity.getId(), entity.getName(), entity.getPrice());
}
@Override
public void deductStock(Long id, int qty) {
repository.deductStock(id, qty);
}
}
// 3. 跨模块调用 (com.shop.order.internal)
@Service
class OrderServiceImpl implements OrderApi {
@Autowired private ProductApi productApi; // ← 只依赖接口
@Autowired private UserApi userApi;
@Override
@Transactional
public OrderDto createOrder(Long userId, Long productId) {
UserDto user = userApi.getById(userId); // 走 API
ProductDto product = productApi.getById(productId); // 走 API
productApi.deductStock(productId, 1); // 走 API
// ...
}
}
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
38
关键约束工具:用 ArchUnit 自动检查边界:
@AnalyzeClasses(packages = "com.shop")
public class ArchitectureTest {
@ArchTest
static final ArchRule modules_should_not_depend_on_internals =
noClasses().that().resideInAPackage("..order..")
.should().dependOnClassesThat()
.resideInAPackage("..product.internal..");
@ArchTest
static final ArchRule internal_should_be_package_private =
classes().that().resideInAPackage("..internal..")
.should().bePackagePrivate();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这是模块化单体的灵魂——没有 ArchUnit 强制约束的"模块化"都是纸老虎,过几个月就被破坏。
# 4.3 模块间通讯
模块间通讯三种范式:
范式 1: 同步 API 调用 (默认)
Order.createOrder()
→ call ProductApi.deductStock()
优点: 简单、强一致
缺点: 强耦合、易循环依赖
范式 2: 异步事件 (解耦)
Order.createOrder()
→ publish OrderCreatedEvent
Product 模块监听 → 异步扣库存
优点: 松耦合、易拆分
缺点: 最终一致、需要事务消息
范式 3: 共享数据库 (反模式!)
Order 模块直接 SELECT product 表
❌ 严禁! 这就是"模块化失败"的起点
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
事件驱动示例(参见 [04.事件驱动架构]):
// 1. 定义事件
public class OrderCreatedEvent {
private final Long orderId;
private final Long productId;
private final int quantity;
// ...
}
// 2. 订单模块发布事件
@Service
class OrderServiceImpl {
@Autowired private ApplicationEventPublisher publisher;
@Transactional
public OrderDto createOrder(Long userId, Long productId) {
Order order = orderRepository.save(...);
// 发事件 (Spring 同进程事件,事务内同步)
publisher.publishEvent(new OrderCreatedEvent(order.getId(), productId, 1));
return toDto(order);
}
}
// 3. 商品模块监听
@Component
class ProductEventListener {
@EventListener
@Async // 异步处理
public void onOrderCreated(OrderCreatedEvent event) {
productService.deductStock(event.getProductId(), event.getQuantity());
}
}
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
关键:进程内事件已经为未来拆分微服务铺好了路——当模块独立成服务时,事件从进程内换成 Kafka 即可,业务代码几乎不变。
# 4.4 模块化的边界
疑惑:模块怎么划?划多大?
论证:模块化的边界 = 限界上下文(参见 [06.DDD])。判定四标尺:
标尺 1: 业务能力独立性
- 这个模块对应一个独立的业务能力 (订单/商品/用户)
- 该能力可以被业务方单独描述、单独验收
标尺 2: 数据所有权
- 这个模块拥有一组自己的表
- 其他模块不直接读写这些表
标尺 3: 团队归属
- 这个模块由一个团队 (2~10 人) 负责
- 一人不同时归属多个模块
标尺 4: 变更频率
- 这个模块的变更与其他模块解耦
- 80% 的需求只改一个模块
2
3
4
5
6
7
8
9
10
11
12
13
14
15
模块化的"成熟度评估":
模块化成熟度 (满分 100):
├─ 包结构按业务划分 (20 分)
├─ ArchUnit 边界检查 (20 分)
├─ 模块对外只暴露 API (15 分)
├─ 模块独立数据表 (15 分)
├─ 跨模块通过事件解耦 (15 分)
└─ 团队按模块组织 (15 分)
≥ 80 分: 可以进阶段 2 (数据库拆分)
60~80: 继续打磨模块化, 不要急
< 60: 还是普通单体, 别自欺欺人
2
3
4
5
6
7
8
9
10
11
红线:没经过模块化阶段就直接拆服务,等同于在烂泥地上盖楼——边界都没磨清楚,进程拆开只会更乱。模块化是微服务的预演——演练好了再上正式舞台。
# 5. 数据库拆分阶段
# 5.1 单库的瓶颈
疑惑:模块化都做好了,数据库一直共享不行吗?
论证:试试看共享单库的后果:
共享单库的四大问题:
问题 1: 模块间数据耦合
- 订单模块改 product 表加列 → 商品模块编译失败
- 数据库表 = 全局耦合点
问题 2: 性能瓶颈无法局部解决
- 商品大促压力大 → 整个库 CPU 飙升
- 订单模块也跟着挂
问题 3: 故障爆炸半径大
- 某个慢 SQL 把连接池打满
- 所有模块都挂
问题 4: 无法独立扩容
- 想给商品加只读副本 → 必须给整个库加
- 资源利用率低
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
单库的临界点:
信号 1: 单库 QPS > 5000 (典型 MySQL 单库上限)
信号 2: 单库数据 > 500 GB (备份/迁移成本爆炸)
信号 3: 慢 SQL 影响其他模块 > 3 次/月
信号 4: 改表结构需要协调 > 2 个团队
2
3
4
结论:数据库不拆,模块化只是表象。微服务的本质是 "独立部署、独立伸缩、独立故障"——而独立故障的前提是独立数据。共享数据库的"微服务"叫分布式单体(参见 8.3)。
# 5.2 一库一模块改造
核心原则:一个模块一个 Schema/数据库。
改造前(共享单库):
┌─────────────────────────────────────────┐
│ shop_db (单库) │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ t_order │ │ t_product│ │ t_user ││
│ │ t_pay │ │ t_sku │ │ t_addr ││
│ └──────────┘ └──────────┘ └─────────┘│
└─────────────────────────────────────────┘
▲ ▲ ▲
└───────────┼───────────┘
所有模块共享
2
3
4
5
6
7
8
9
10
改造后(一库一模块):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ order_db │ │ product_db│ │ user_db │
│ t_order │ │ t_product│ │ t_user │
│ t_pay │ │ t_sku │ │ t_addr │
└────▲─────┘ └────▲─────┘ └────▲─────┘
│ │ │
┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐
│ 订单模块 │ │ 商品模块 │ │ 用户模块 │
└──────────┘ └──────────┘ └──────────┘
2
3
4
5
6
7
8
9
改造步骤(小步快跑):
Step 1: Schema 拆分 (第 1 周)
- 单实例多 Schema (物理上还是一个 MySQL 实例)
- DDL: CREATE SCHEMA order_db; ALTER TABLE t_order RENAME TO order_db.t_order;
- 数据库连接配置: 每个模块用独立 DataSource
Step 2: 应用层适配 (第 2 周)
- 配置多 DataSource
- @MapperScan 区分包扫描
- 事务管理器分离
Step 3: 实例拆分 (第 3~4 周, 可选)
- 物理实例分离 (order_db 一台、product_db 一台)
- 监控独立、备份独立、扩容独立
2
3
4
5
6
7
8
9
10
11
12
13
多数据源配置示例:
@Configuration
@MapperScan(basePackages = "com.shop.order.internal.repository",
sqlSessionFactoryRef = "orderSqlSessionFactory")
public class OrderDataSourceConfig {
@Bean(name = "orderDataSource")
@ConfigurationProperties(prefix = "spring.datasource.order")
public DataSource orderDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "orderSqlSessionFactory")
public SqlSessionFactory orderSqlSessionFactory(
@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}
@Bean(name = "orderTransactionManager")
public PlatformTransactionManager orderTransactionManager(
@Qualifier("orderDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# application.yml
spring:
datasource:
order:
url: jdbc:mysql://order-db:3306/order_db
username: order_user
password: ${ORDER_DB_PASSWORD}
product:
url: jdbc:mysql://product-db:3306/product_db
username: product_user
password: ${PRODUCT_DB_PASSWORD}
2
3
4
5
6
7
8
9
10
11
# 5.3 跨库查询四方案
核心痛点:原来一个 JOIN 查 3 张表,现在 3 张表在 3 个库——怎么办?
-- 原来 (单库 JOIN)
SELECT o.*, p.name, u.name
FROM t_order o
JOIN t_product p ON o.product_id = p.id
JOIN t_user u ON o.user_id = u.id
WHERE o.id = 123;
-- 拆库后,这个 JOIN 跑不动了 → 怎么办?
2
3
4
5
6
7
8
四方案对比:
┌──────────────────────────────────────────────────────────┐
│ 方案 适用场景 代价 │
├──────────────────────────────────────────────────────────┤
│ 1. 应用层 JOIN 低频查询、小数据集 多次 IO │
│ 2. 数据冗余 热点字段、写少读多 一致性维护 │
│ 3. CQRS 读模型 复杂报表、列表查询 最终一致 │
│ 4. 数据复制 实时跨库查询 全套同步组件 │
└──────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
方案 1 · 应用层 JOIN(最常用):
public OrderDetailDto getOrderDetail(Long orderId) {
// 1. 查订单
OrderDto order = orderApi.getById(orderId);
// 2. 查商品 (跨库)
ProductDto product = productApi.getById(order.getProductId());
// 3. 查用户 (跨库)
UserDto user = userApi.getById(order.getUserId());
// 4. 组装
return new OrderDetailDto(order, product, user);
}
// 适用: QPS < 1000、查询字段少
// 缺点: 3 次 IO,延迟约 = 3 × 单次延迟
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方案 2 · 数据冗余(热点字段):
-- 订单表冗余商品名、用户名 (写入时同步)
CREATE TABLE order_db.t_order (
id BIGINT,
product_id BIGINT,
product_name VARCHAR(200), -- ← 冗余字段
user_id BIGINT,
user_name VARCHAR(100), -- ← 冗余字段
...
);
-- 适用: 字段稳定 (名字、品类)、读多写少
-- 缺点: 商品改名后,订单里的名字怎么办? (策略: 不变,记录下单时的名字)
2
3
4
5
6
7
8
9
10
11
12
方案 3 · CQRS 读模型(参见 [03.CQRS]):
写库 (order_db, product_db, user_db)
↓ Outbox/CDC 投影
读库 (order_query_db)
└── order_detail_view (冗余宽表)
应用查询直接 SELECT * FROM order_detail_view WHERE id = ?
适用: 复杂报表、多维筛选
代价: 增加投影组件 + 最终一致
2
3
4
5
6
7
8
方案 4 · 数据复制(如 Canal + ES):
order_db (MySQL) ──canal binlog─→ ElasticSearch
product_db (MySQL) ─canal binlog─→ ElasticSearch
user_db (MySQL) ────canal binlog─→ ElasticSearch
↓
统一查询接口 (ES 跨索引 JOIN)
2
3
4
5
选型决策树:
跨库查询需求来了
│
▼
是否高频 (>100 QPS)?
/ \
否 是
↓ ↓
应用层 JOIN 是否复杂报表?
(方案 1) / \
否 是
↓ ↓
是否字段稳定? CQRS 读模型
/ \ (方案 3)
是 否
↓ ↓
数据冗余 数据复制
(方案 2) (方案 4)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.4 数据迁移演练
最危险的一步——数据迁移做不好,整个演进白费。
迁移五步法:
Step 1: 影子库 (Shadow DB) 准备
- 创建目标库 schema
- 配置同步工具 (Canal/DataX)
- 验证 schema 一致
Step 2: 全量迁移 (Initial Sync)
- 业务低峰期执行
- 校验数据条数、checksum
- 时长: 100 GB 约 2~4 小时
Step 3: 增量同步 (Incremental Sync)
- Canal 监听 binlog
- 实时同步增量数据
- 延迟 < 1s
Step 4: 双读校验 (Double Read)
- 读流量 1% 同时查新旧库
- 比对结果, 不一致告警
- 持续 1~2 周
Step 5: 切流 + 旧库下线
- 写流量先切 (双写过渡)
- 读流量灰度切
- 旧库保留 30 天,确认无问题再删
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
校验工具(伪代码):
def verify_consistency(old_db, new_db, table):
# 1. 条数校验
old_count = old_db.execute(f"SELECT COUNT(*) FROM {table}")
new_count = new_db.execute(f"SELECT COUNT(*) FROM {table}")
assert old_count == new_count
# 2. 抽样校验 (随机 1000 条)
samples = old_db.execute(f"SELECT * FROM {table} ORDER BY RAND() LIMIT 1000")
for row in samples:
new_row = new_db.execute(f"SELECT * FROM {table} WHERE id = {row.id}")
assert row == new_row, f"Mismatch on id={row.id}"
# 3. 增量校验 (近 1 小时数据)
recent = old_db.execute(f"SELECT * FROM {table} WHERE updated_at > NOW() - INTERVAL 1 HOUR")
for row in recent:
new_row = new_db.execute(f"SELECT * FROM {table} WHERE id = {row.id}")
assert row == new_row
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
红线:数据迁移必须有回滚预案——任何时刻都能切回旧库。不能回滚的迁移 = 自杀式迁移。
# 6. 服务拆分阶段
# 6.1 第一刀切哪
疑惑:模块化做好了、数据库分了,开始拆服务——第一刀切哪个模块?
论证:不是从最大的切,是从最适合的切。判定四标尺:
标尺 1: 业务独立性
- 该模块业务相对独立 (如商品 vs 订单)
- 与其他模块通讯少 (跨模块调用 < 20%)
标尺 2: 变更频率
- 该模块变更频率与其他模块差异大
- 独立部署收益高
标尺 3: 性能压力
- 该模块是性能瓶颈或压力大
- 独立扩容收益高
标尺 4: 团队成熟度
- 有专门团队负责该模块
- 团队具备独立运维能力
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优先级矩阵:
业务独立性
▲
│
高 │ 优先拆 重点拆 战略拆
│ (易+收益高) (核心) (难+收益高)
│
中 │ 顺手拆 慎重拆 不建议
│
低 │ 不拆 不拆 不拆
│
└──────────────────────────► 收益 (变更/性能)
低 中 高
2
3
4
5
6
7
8
9
10
11
12
第 1 章案例的正确拆法:
原始 80 万行单体, 应该这样切:
阶段 3.1 (第 1~3 月): 拆 "商品服务"
理由: 商品模块业务最独立、读多写少、压力最大
收益: 商品独立扩容,大促时只扩商品
阶段 3.2 (第 4~6 月): 拆 "用户服务"
理由: 用户模块只被读、业务稳定
收益: 用户接口独立,登录不影响其他
阶段 3.3 (第 7~9 月): 拆 "订单服务"
理由: 核心业务,但与商品/用户耦合多,放最后
收益: 完整业务闭环成型
而第 1 章 CTO 一口气拆 20 个 → 等于 9 个月活拆成 3 个月,必然崩
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.2 进程内变进程外
核心变化——把模块从"进程内 Bean 调用"变成"独立服务进程 RPC 调用":
拆分前 (进程内):
shop-monolith.jar
├── order module ──Bean调用──► product module
└── ...
拆分后 (进程外):
shop-monolith.jar product-service.jar
├── order module ──RPC调用────► ProductController
└── (product module 已抽出)
2
3
4
5
6
7
8
9
改造代码(接口几乎不变,魔术在 Spring Cloud/Dubbo):
// 拆分前: Spring Bean
@Autowired private ProductApi productApi; // 本地 Bean
// 拆分后 (OpenFeign): 看起来一模一样
@FeignClient(name = "product-service") // ← 改成 Feign Client
public interface ProductApi {
@GetMapping("/api/products/{id}")
ProductDto getById(@PathVariable Long id);
@PostMapping("/api/products/{id}/deduct")
void deductStock(@PathVariable Long id, @RequestParam int qty);
}
// 调用方代码无任何变化:
@Autowired private ProductApi productApi; // ← 业务代码 0 改动
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这就是第 4 章"模块化时坚持 API 接口"的核心价值——拆服务时业务代码 0 改动,只改注解 + 部署。
新增的"分布式问题":
原来本地调用 现在 RPC 调用
─────────────────────────────────────────────────
延迟 ~ns → 延迟 ~ms (1000x)
不会失败 → 网络可能超时/抖动
强一致事务 → 分布式事务问题
本地异常 → 远程异常 (序列化)
直接 debug → 分布式追踪 (Tracing)
2
3
4
5
6
7
必备的"分布式装备":
1. 服务注册发现: Nacos / Eureka
2. 客户端负载均衡: Ribbon / Spring Cloud LoadBalancer
3. 熔断降级: Resilience4j / Sentinel
4. 链路追踪: SkyWalking / Zipkin
5. 配置中心: Nacos / Apollo
6. API 网关: Spring Cloud Gateway / Kong
2
3
4
5
6
# 6.3 RPC与事件协同
拆服务后,通讯模型有两种:
模型 1: 同步 RPC (强一致需求)
下单 → 调用 ProductService.deductStock() → 等返回 → 写订单
优点: 立即知道扣减结果
缺点: 强耦合, 商品挂了订单也挂
模型 2: 异步事件 (最终一致需求)
下单 → 写订单 → 发 OrderCreated 事件
ProductService 消费事件 → 异步扣库存
优点: 解耦, 商品慢/挂不影响下单
缺点: 库存可能短暂不一致
2
3
4
5
6
7
8
9
10
选型原则:
强一致场景 (扣款/扣库存):
→ 同步 RPC + 分布式事务 (Saga)
最终一致场景 (积分/通知/统计):
→ 异步事件 (Kafka + Outbox)
查询场景:
→ 同步 RPC (短超时 + 兜底)
2
3
4
5
6
7
8
典型混合架构:
┌──────────┐ RPC ┌──────────┐
│ 订单服务 │ ───────► │ 商品服务 │ ← 同步扣库存
└────┬─────┘ └──────────┘
│
│ Event
▼
┌──────────┐ Consume ┌──────────┐
│ Kafka │ ────────► │ 积分服务 │ ← 异步加积分
└──────────┘ └──────────┘
┌──────────┐
│ 通知服务 │ ← 异步发短信
└──────────┘
2
3
4
5
6
7
8
9
10
11
12
# 6.4 双跑验证
最稳的拆分方式——新老服务并行运行 + 流量影子比对。
双跑流程:
┌───────────────────────────────────────────────────┐
│ 阶段 1: 影子模式 (0% 真实流量,100% 影子流量) │
├───────────────────────────────────────────────────┤
│ │
│ 请求 → 老系统 (主) → 返回结果给用户 │
│ ↓ │
│ 影子流量 │
│ ↓ │
│ 新服务 (影子) → 只记录结果, 比对差异 │
│ │
│ 时长: 1~2 周 │
│ 目标: 发现 99% 的不一致点 │
└───────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────┐
│ 阶段 2: 灰度切流 (1% → 10% → 50% → 100%) │
├───────────────────────────────────────────────────┤
│ │
│ 请求 → 按规则路由 → 老系统 (90%) │
│ → 新服务 (10%) │
│ │
│ 监控: 错误率/延迟/业务指标 对比 │
│ 时长: 每档观察 3~7 天 │
└───────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────┐
│ 阶段 3: 老系统下线 (保留 30 天紧急回滚) │
└───────────────────────────────────────────────────┘
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
影子流量代码示例:
@RestController
public class OrderController {
@Autowired private OldOrderService oldService;
@Autowired private NewOrderService newService;
@Autowired private ShadowComparator comparator;
@PostMapping("/order")
public OrderDto create(@RequestBody OrderReq req) {
// 1. 老服务处理 (主)
OrderDto oldResult = oldService.create(req);
// 2. 新服务影子运行 (异步,不影响响应)
CompletableFuture.runAsync(() -> {
try {
OrderDto newResult = newService.create(req);
comparator.compare(oldResult, newResult); // 比对差异
} catch (Exception e) {
log.warn("Shadow service failed", e);
}
});
return oldResult; // 返回老结果给用户
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
结论:没经过影子模式和灰度的服务拆分都是赌博——影子模式能在不影响用户的前提下发现 99% 的问题。这是第 1 章 CTO 缺失的最关键一环。
# 7. 绞杀者模式
# 7.1 绞杀者的本质
疑惑:祖传系统 80 万行代码,怎么改?整个推倒重写?
论证:推倒重写 = 自杀。Joel Spolsky 著名的 [Things You Should Never Do]:
推倒重写的灾难案例:
- Netscape Navigator 6 (2000): 重写 3 年, 市场被 IE 抢光
- Borland C++ Builder: 重写后性能崩溃, 用户流失
- 国内某金融系统: 重写 4 年, 业务中断, 公司倒闭
2
3
4
正确姿势:绞杀者模式(Strangler Fig Pattern)——Martin Fowler 2004 年提出,得名于热带雨林的绞杀榕。
绞杀榕的生长过程:
Year 0: 从老树上萌芽
Year 5: 根系包裹老树
Year 10: 老树死亡, 绞杀榕完全替代
新系统的生长过程:
Month 0: 在老系统旁边搭新系统
Month 6: 新系统接管 30% 接口
Month 12: 新系统接管 80% 接口
Month 18: 老系统完全下线
2
3
4
5
6
7
8
9
10
核心思想:新老共存、逐步迁移、随时回滚。
┌──────────────────────────────────────────────┐
│ 反向代理 / API 网关 │
│ (路由规则: /new/* → 新, /old/* → 老) │
└────────────────┬─────────────────────────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 新系统 │ │ 老系统 │
│ (渐进开发) │ │ (持续运行) │
└──────────────┘ └──────────────┘
│ │
└────────┬────────┘
▼
┌──────────────┐
│ 数据库 │ ← 共享 或 双写
└──────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.2 反向代理路由
核心机制——通过 Nginx/API 网关在入口分流:
# nginx.conf - 渐进式路由示例
upstream old_system {
server old1.shop.com:8080;
server old2.shop.com:8080;
}
upstream new_order_service {
server new-order1.shop.com:8080;
server new-order2.shop.com:8080;
}
server {
listen 80;
# 阶段 1: 新订单查询接口走新服务
location /api/orders/v2/ {
proxy_pass http://new_order_service;
}
# 阶段 2: 老订单接口仍走老系统
location /api/orders/ {
proxy_pass http://old_system;
}
# 默认走老系统 (兜底)
location / {
proxy_pass http://old_system;
}
}
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
Spring Cloud Gateway 版本:
spring:
cloud:
gateway:
routes:
# 新接口路由到新服务
- id: new_order
uri: lb://new-order-service
predicates:
- Path=/api/orders/v2/**
filters:
- StripPrefix=2
# 灰度规则: 5% 老接口流量也试试新服务
- id: gray_order
uri: lb://new-order-service
predicates:
- Path=/api/orders/**
- Weight=group1, 5
# 95% 走老系统
- id: old_order
uri: lb://old-monolith
predicates:
- Path=/api/orders/**
- Weight=group1, 95
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 7.3 老接口下线节奏
绞杀的节奏要稳——不能太快也不能太慢:
节奏 1: 太快 (1 个月切完)
风险: 没观察期, 老业务找不到归属
后果: 不一致问题集中爆发
节奏 2: 太慢 (3 年还在迁)
风险: 双跑成本叠加, 团队疲惫
后果: 项目烂尾, 老系统永生
节奏 3: 适中 (6~18 个月)
推荐: 每月迁 10~20% 接口
关键: 观察 + 复盘 + 下个接口
2
3
4
5
6
7
8
9
10
11
接口下线的"四步走":
Step 1: Deprecated 标记 (T+0)
- 老接口加 @Deprecated 注解
- 文档标记"将于 X 月下线"
- 监控老接口调用方
Step 2: 通知调用方 (T+1 月)
- 邮件 + Slack 通知
- 提供迁移指南
- 设置 grace period
Step 3: 强制迁移 (T+3 月)
- 老接口加限流 (逐步降流量)
- 返回 Sunset Header (HTTP 标准)
- 拒绝新调用方接入
Step 4: 完全下线 (T+6 月)
- 老接口返回 410 Gone
- 代码归档保留 3 个月
- 真正删除代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sunset Header 标准用法:
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://api.example.com/v2/orders>; rel="successor-version"
Deprecation: true
2
3
4
# 7.4 数据双写过渡
最复杂的一步——数据从老库迁到新库,过渡期需要双写:
┌──────────────────────────────────────────────┐
│ 应用层 (双写逻辑) │
└───────┬────────────────────────┬─────────────┘
│ 写老库 (主) │ 写新库 (影子)
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 老数据库 │ │ 新数据库 │
└──────────────┘ └──────────────┘
↑ ↑
└──── Canal 同步 ────────┘
(校验一致性)
2
3
4
5
6
7
8
9
10
11
双写代码模板:
@Service
public class OrderDoubleWriteService {
@Autowired private OldOrderRepository oldRepo;
@Autowired private NewOrderRepository newRepo;
@Value("${double-write.mode:OLD_ONLY}")
private String mode;
@Transactional
public Order create(OrderReq req) {
switch (mode) {
case "OLD_ONLY": // 阶段 1: 只写老库
return oldRepo.save(toOldEntity(req));
case "DOUBLE_WRITE_OLD_PRIMARY": // 阶段 2: 双写, 以老为准
Order oldOrder = oldRepo.save(toOldEntity(req));
try {
newRepo.save(toNewEntity(req));
} catch (Exception e) {
log.warn("New DB write failed, will be caught by sync", e);
}
return oldOrder;
case "DOUBLE_WRITE_NEW_PRIMARY": // 阶段 3: 双写, 以新为准
Order newOrder = newRepo.save(toNewEntity(req));
try {
oldRepo.save(toOldEntity(req)); // 兜底, 给可能未迁移的查询用
} catch (Exception e) {
log.warn("Old DB write failed", e);
}
return newOrder;
case "NEW_ONLY": // 阶段 4: 只写新库
return newRepo.save(toNewEntity(req));
}
}
}
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
双写的四阶段切换:
阶段 模式 读流量 写流量
────────────────────────────────────────────────────────────
1 (T+0) OLD_ONLY 100% 老 100% 老
2 (T+1月) DOUBLE_WRITE_OLD_PRIMARY 100% 老 双写,以老为准
3 (T+3月) DOUBLE_WRITE_OLD_PRIMARY 1%~50% 新 双写,以老为准
4 (T+5月) DOUBLE_WRITE_NEW_PRIMARY 50%~99% 新 双写,以新为准
5 (T+6月) NEW_ONLY 100% 新 100% 新
2
3
4
5
6
7
关键纪律:
1. 双写期间, 一致性校验工具必须 7×24 运行
2. 不一致超过阈值 (>0.1%) 立刻告警 + 暂停切流
3. 每阶段持续 ≥ 2 周, 不允许跳阶段
4. 每阶段保留回滚路径 (切回上一阶段)
2
3
4
# 8. 演进反模式
# 8.1 大爆炸重构
症状(即第 1 章案例):
- "我们 3 个月把单体改成 20 个微服务"
- "不留兼容代码, 直接切"
- "上线那天全员加班守一晚就行"
2
3
根因:
- 把"架构演进"当成"项目交付"
- 没有阶段验证, 错了无法局部修正
- 数据库一拆, 回滚成本指数级上升
修复:
正确姿势:
1. 拒绝"3 个月拆完"的承诺
2. 强制走阶段: 单体 → 模块化 → 数据库拆 → 服务拆
3. 每阶段稳定 ≥ 6 个月再进下一阶段
4. 每一步都有"灰度 + 双跑 + 回滚"三件套
5. 演进周期至少 12~24 个月,接受现实
2
3
4
5
6
红线:任何"3 个月完成大重构"的承诺都是骗局——大重构是 1~2 年的事, 接受这个现实才能成功。
# 8.2 提前微服务
症状:
5 人创业团队, 第一天就拆 10 个微服务
理由: "听说微服务是未来"
结果: 90% 时间在搞基础设施, MVP 永远出不来
2
3
根因:
- 把"业界最佳实践"误以为"放之四海皆准"
- 不理解微服务的成本曲线
- 不理解"边界发现"需要时间
修复:
判定原则: "Monolith First"
1. 团队 < 10 人 → 单体起步
2. 业务边界未稳定 → 单体起步
3. 没有 DevOps 基础 → 单体起步
4. 团队从未运维过微服务 → 单体起步
正确节奏:
- 0~6 月: 单体跑通 MVP
- 6~12 月: 重构成模块化单体
- 12~24 月: 拆 1~2 个核心服务
- 24~36 月: 完整微服务体系
2
3
4
5
6
7
8
9
10
11
金句:Sam Newman(《Building Microservices》作者)的原话——
"Don't start with microservices. Microservices are a means to an end, not the end itself."
# 8.3 分布式单体
症状:
拆了 10 个服务, 但:
- 共享一个数据库
- 改一个接口要 10 个服务一起发布
- 任何服务挂全链路挂
- 调用关系网状, 一个请求 15 次 RPC
2
3
4
5
这就是"分布式单体"——有微服务的成本, 没有微服务的红利。
根因:
- 拆服务时只切代码, 没切数据
- 拆服务时没拆团队 (康威定律反作用)
- 跨服务调用泛滥 (边界没找对)
修复:
诊断指标:
☐ 一次需求改 > 3 个服务 → 分布式单体
☐ 一次请求 > 5 次 RPC → 分布式单体
☐ 任何服务挂导致全链路挂 → 分布式单体
☐ 多服务共享数据库 → 分布式单体
修复方案:
1. 数据先拆 (一库一服务)
2. 重新审视边界 (限界上下文)
3. 高频协同的服务合并回去 (是的, 合并是允许的!)
4. 异步事件替换大量 RPC
5. 团队结构调整 (一服务一团队)
2
3
4
5
6
7
8
9
10
11
12
反思:分布式单体比单体更糟——单体至少简单, 分布式单体既复杂又脆弱。宁可回到单体也别留在分布式单体。
# 8.4 数据库共享
症状:
拆了服务但库没拆
理由: "拆库太麻烦,先这样"
结果:
- A 服务改表结构 → B 服务编译失败
- 跨服务 JOIN 满天飞
- 想给 B 加只读副本 → 必须给整个库加
- 服务"独立"是个笑话
2
3
4
5
6
7
根因:
- 把"代码独立"误以为"服务独立"
- 低估了数据耦合的破坏力
- 没有数据迁移能力 (没用过 Canal/DataX)
修复:
强制纪律:
1. 一个服务一个数据库 (Schema 或实例)
2. 跨服务数据访问只走 API
3. 严禁跨服务 JOIN
4. 历史共享数据 → 用 CQRS 读模型解决
如果暂时拆不动:
1. 至少先做 Schema 隔离 (单实例多 Schema)
2. 加 ArchUnit 规则禁止跨 Schema SQL
3. 制定 6 个月内物理拆库计划
2
3
4
5
6
7
8
9
10
演进反模式集锦:
| 反模式 | 症状 | 修复 |
|---|---|---|
| 大爆炸重构 | 3 个月拆 20 服务 | 强制走阶段 + 1~2 年周期 |
| 提前微服务 | 5 人 10 服务 | Monolith First |
| 分布式单体 | 拆了服务没拆库 | 先拆库 + 重审边界 |
| 数据库共享 | "拆库太麻烦" | 强制一服务一库 |
| 形式微服务 | "我们也微服务了" | 看是否真独立部署/伸缩/故障 |
| 永久双跑 | 双跑 3 年 | 设置硬性下线 deadline |
# 9. 演进度量与回滚
# 9.1 演进指标体系
疑惑:怎么知道演进是"成功"还是"失败"?
论证:必须有量化指标——拍脑袋的"感觉好多了"不算数。
架构演进的四大类指标:
┌──────────────────────────────────────────────────────┐
│ 架构演进健康度指标体系 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. 交付效率指标 │
│ - 部署频率 (deploy/天) 目标: 提升 │
│ - 部署时长 (分钟) 目标: 下降 │
│ - 需求交付周期 (天) 目标: 下降 │
│ - 代码合入冲突 (次/周) 目标: 下降 │
│ │
│ 2. 质量指标 │
│ - 生产事故 (P0/P1/P2) 目标: 下降 │
│ - 平均故障恢复时长 MTTR 目标: 下降 │
│ - 单元测试覆盖率 目标: 上升 │
│ - 跨模块变更比例 目标: 下降 │
│ │
│ 3. 技术健康度指标 │
│ - 服务数 / 模块数 目标: 与团队匹配 │
│ - 服务平均代码行数 目标: 1~5 万行 │
│ - 跨服务调用比例 目标: < 20% │
│ - 数据库共享比例 目标: 0% │
│ │
│ 4. 业务指标 │
│ - 业务可用性 (SLA) 目标: 提升 │
│ - 业务峰值承载 (QPS) 目标: 提升 │
│ - 资源成本 (服务器/月) 目标: 优化 │
│ │
└──────────────────────────────────────────────────────┘
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
第 1 章案例的"事后量化":
原始单体 拆后(失败) 理想拆法
─────────────────────────────────────────────────────────────
部署频率 1次/周 0.3次/周(更慢) 5次/周
部署时长 40 分 20分(*20服务) 5 分
需求周期 15 天 30 天 7 天
P0 事故/月 0.5 次 3 次 0.2 次
QPS 上限 3000 5000 30000
服务器月成本 ¥10w ¥80w ¥25w
→ 实际拆完所有指标恶化, 这就是"失败的演进"
→ 没有指标体系, 团队还在自欺欺人"我们升级了"
2
3
4
5
6
7
8
9
10
11
# 9.2 灰度与开关
演进过程中最重要的两个工具:
工具 1 · Feature Flag(功能开关)
@Service
public class OrderService {
@Autowired private FeatureFlagService featureFlag;
@Autowired private OldOrderLogic oldLogic;
@Autowired private NewOrderLogic newLogic;
public Order create(OrderReq req) {
if (featureFlag.isEnabled("USE_NEW_ORDER_LOGIC", req.getUserId())) {
return newLogic.create(req); // 新逻辑
} else {
return oldLogic.create(req); // 老逻辑
}
}
}
// 配置中心动态控制:
// USE_NEW_ORDER_LOGIC:
// default: false
// rules:
// - userId IN [101, 102, 103]: true # 内部测试
// - userId % 100 < 5: true # 5% 灰度
// - userId % 100 < 50: true # 50% 灰度
// - default: true # 100% 切换
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
工具 2 · 灰度发布策略
策略 1: 按用户 ID 灰度 (典型 5% → 100%)
策略 2: 按地域灰度 (先小城市,后大城市)
策略 3: 按 IP 灰度 (内网先,外网后)
策略 4: 按时间段灰度 (低峰先,高峰后)
策略 5: 按设备灰度 (Android 先, iOS 后)
2
3
4
5
灰度的黄金法则:
1. 先内部, 后外部 (员工 → 灰度用户 → 全量)
2. 先少量, 后大量 (1% → 5% → 25% → 50% → 100%)
3. 先低风险, 后高风险 (查询接口 → 写入接口 → 支付接口)
4. 每档观察 ≥ 24 小时
5. 任何指标异常立刻回滚 (开关一关即回滚)
2
3
4
5
# 9.3 回滚预案
最重要的一节——没回滚预案就不算上线。
回滚预案四要素:
要素 1: 回滚触发条件 (Trigger)
- P0 事故出现
- 核心指标偏离 > X%
- 灰度组反馈 > N 起严重问题
要素 2: 回滚执行步骤 (Steps)
- 谁来按按钮 (Owner)
- 按哪个按钮 (具体操作)
- 多久能回滚完 (RTO)
要素 3: 回滚验证 (Verify)
- 回滚后核心指标是否恢复
- 数据是否一致
- 用户是否感知
要素 4: 回滚后续 (Postmortem)
- 立刻召集复盘会
- 根因分析
- 下次重试的准入条件
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
典型回滚剧本:
## 回滚预案 - 订单服务 v2 上线
### 触发条件 (任一满足即回滚)
- 下单成功率 < 99% (基线 99.95%)
- 下单延迟 P99 > 1s (基线 300ms)
- 出现 P0 事故 (数据不一致/资损)
- 灰度用户 > 10 起反馈
### 回滚步骤
1. 关闭 Feature Flag (USE_NEW_ORDER_SERVICE = false)
- Owner: 老王
- 工具: 配置中心 push
- 预计时间: 30 秒
2. Nginx 路由切回老系统
- Owner: 运维老张
- 工具: nginx -s reload
- 预计时间: 1 分钟
3. 验证回滚效果
- 监控: SkyWalking 看入口流量
- 业务: 测试下单链路
- 预计时间: 5 分钟
### 回滚总 RTO: ≤ 10 分钟
### 数据回滚 (如已写新库)
- 阶段 1 (双写期): 无需操作, 老库数据完整
- 阶段 2 (单写新库): 暂时双写 + Canal 同步老库
### 责任人
- 主 Owner: 架构师老李 (24h on-call)
- 备 Owner: CTO
- 通知: 全员 Slack #incident 频道
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
# 9.4 复盘机制
演进过程必须定期复盘——否则错误会重复。
三种复盘节奏:
节奏 1: 每周 Standup (15 分钟)
- 本周演进进展
- 遇到的阻塞
- 下周计划
节奏 2: 每月里程碑复盘 (1 小时)
- 本月指标变化
- 计划 vs 实际偏差
- 下月调整
节奏 3: 阶段性复盘 (半天)
- 阶段目标达成度
- 关键决策回顾 (ADR review)
- 是否进入下一阶段
2
3
4
5
6
7
8
9
10
11
12
13
14
复盘模板(参考 Google SRE Postmortem):
# 阶段 2 (数据库拆分) 复盘
## 阶段目标
将 t_order, t_product, t_user 三组表拆到独立 Schema
## 计划 vs 实际
- 计划工期: 4 周
- 实际工期: 7 周 (超期 75%)
- 原因: 跨库 JOIN 改造比预期多 (从 30 个发现 78 个)
## 关键指标
- 拆库前 QPS: 3000
- 拆库后 QPS: 5000 (+67%)
- P0 事故: 1 起 (双写不一致, 影响 200 单)
- 团队加班: 平均每人 60 小时
## 经验教训
1. 跨库 JOIN 评估方法不充分 (只查代码, 没查 SQL 日志)
2. 双写校验工具应该上线前 1 个月就准备好
3. 灰度阶段应该再多 2 周观察
## 行动项
- [ ] 完善"跨库依赖扫描"工具 (Owner: 老王, T+2 周)
- [ ] 双写校验工具开源化 (Owner: 老李, T+1 月)
- [ ] 总结"数据库拆分 Checklist" (Owner: 全员, T+2 周)
## 准入下一阶段
☑ 拆库已稳定运行 1 个月
☑ P0 事故已修复
☑ 双写已切到单写新库
→ 可以进入"阶段 3 服务拆分"
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
复盘的"红线":
1. 失败不甩锅 (对事不对人)
2. 行动项必须有 Owner + Deadline
3. 行动项必须下次复盘核查
4. 不要重复犯同一个错 (超过 2 次升级到流程治理)
2
3
4
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章的 50 人电商团队"3 个月拆 20 服务"事故,八个疑问现在能逐条作答:
| 疑问 | 答案 |
|---|---|
| ① 什么时候该拆?什么时候不该拆? | 第 3.3:四信号 + 临界点指标表,团队 < 10 不拆 |
| ② 单体一定不好吗?模块化单体可以撑多久? | 第 4:模块化单体可撑 30~50 人团队、100 万行代码 |
| ③ 数据库不拆能跑微服务吗? | 第 5.1:不能,那叫"分布式单体",是反模式 |
| ④ 拆服务第一刀应该切哪里? | 第 6.1:业务最独立 + 收益最高的模块先拆 |
| ⑤ 老系统怎么不停机迁移? | 第 7:绞杀者模式 + 反向代理 + 数据双写 |
| ⑥ 大爆炸重构错在哪? | 第 8.1:跳阶段、无验证、不可回滚 |
| ⑦ 演进出问题怎么回滚? | 第 9.3:Feature Flag + 路由切换 + RTO ≤ 10min |
| ⑧ 怎么度量演进的成功与失败? | 第 9.1:交付效率/质量/技术健康/业务四类指标 |
正确做法(如果重来一次):
正确的 18 个月演进路径:
Month 0~2: 准备阶段
- 业务能力梳理 (DDD 战略设计, 见 06 篇)
- 评审委员会成立 (见 07 篇)
- 演进 ADR-0001 立项
- 选定第一阶段目标: 模块化单体
Month 2~6: 阶段 1 (模块化单体)
- 按业务能力重组包结构
- 引入 ArchUnit 强制边界
- 80 万行代码拆成 8 个 module
- 模块间通讯走 API + 进程内事件
指标: 跨模块改动比例 60% → 25%
Month 6~10: 阶段 2 (数据库拆分)
- 200 张表拆到 8 个 Schema
- 跨库查询 78 个 → 改造方案
- 单实例多 Schema 起步
- 双写校验工具上线
指标: 单库 QPS 3k → 8 个库各 1k, 互不影响
Month 10~16: 阶段 3 (服务拆分, 优先级)
- Month 10~12: 拆"商品服务" (业务独立、读多)
- Month 12~14: 拆"用户服务" (业务稳定)
- Month 14~16: 拆"订单服务" (核心、放最后)
指标: 部署频率 1次/周 → 5次/周
Month 16~18: 阶段 4 (微服务体系)
- 引入完整治理: Nacos + Sentinel + SkyWalking
- 灰度发布平台
- 服务网格 (可选)
指标: P0 事故 0.5/月 → 0.1/月
总计: 18 个月、3 个核心服务 + 5 个支撑服务 (而非 20 个)
结果: 部署提速 5 倍、QPS 提升 10 倍、事故下降 80%
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
修复方案(按代价从小到大):
方案 A · 立刻停止当前拆分(最紧迫)
立刻动作:
- 暂停剩余拆分计划
- 已拆服务: 合并回 5~8 个核心服务
- 数据库回归: 一服务一库, 砍掉"分布式单体"
- 引入灰度 + 双跑
代价: 短期看着"倒退",但能止损
收益: 团队从崩溃恢复, 重新出发
2
3
4
5
6
7
8
方案 B · 回退到阶段 1 重新出发(中等代价)
立刻动作:
- 把 20 个服务合并回模块化单体
- 引入 ArchUnit 重塑边界
- 数据库逐步收回单库 (Schema 隔离)
- 重新走渐进路径
代价: 半年回退期
收益: 走对路径, 后续可控
2
3
4
5
6
7
8
方案 C · 引入外部专家全面诊断(最重)
立刻动作:
- 聘请架构顾问 (2~3 周诊断)
- 全面评估当前状态
- 给出 12 个月恢复 + 演进路径
- 团队培训 + 流程重建
代价: 顾问费 + 团队重建时间
收益: 系统性恢复, 避免再犯
2
3
4
5
6
7
8
生产建议:第 1 章案例已经踩坑——方案 A 立刻做(止损),方案 B 中期推进(重新走对),方案 C 视团队反思深度(如反思不够建议引入外部)。三方案叠加 = 真正解决问题。
# 10.2 一次完整演进全过程
把"从单体到微服务"的完整演进串成一棵知识树:
决策"启动 XXX 系统架构演进"
│
├─ 阶段 0: 单体起步 (第 3 章)
│ ├─ Monolith First 原则
│ ├─ 分层架构 (Controller/Service/Repo)
│ ├─ 单库单服务
│ └─ 监控临界点信号
│
├─ 临界点判定 → 决定升级
│ ├─ 代码 > 30 万行?
│ ├─ 团队 > 20 人?
│ ├─ 跨模块改动 > 40%?
│ └─ ADR-0001 演进立项 (引用 07 篇评审)
│
├─ 阶段 1: 模块化单体 (第 4 章)
│ ├─ 业务梳理 (DDD 限界上下文, 引用 06 篇)
│ ├─ 包结构按业务重组
│ ├─ ArchUnit 强制边界
│ ├─ 模块对外只暴露 API
│ ├─ 模块间通讯
│ │ ├─ 同步调用 (默认)
│ │ └─ 进程内事件 (为未来微服务铺路, 引用 04 篇)
│ └─ 模块化成熟度评估
│
├─ 阶段 2: 数据库拆分 (第 5 章)
│ ├─ Schema 拆分 (单实例多 Schema)
│ ├─ 多 DataSource 配置
│ ├─ 跨库查询四方案
│ │ ├─ 应用层 JOIN
│ │ ├─ 数据冗余
│ │ ├─ CQRS 读模型 (引用 03 篇)
│ │ └─ 数据复制
│ ├─ 数据迁移五步法
│ │ ├─ 影子库 → 全量 → 增量 → 双读 → 切流
│ └─ 一致性校验工具
│
├─ 阶段 3: 服务拆分 (第 6 章)
│ ├─ 第一刀切哪 (四标尺)
│ ├─ 模块独立成服务进程
│ ├─ 进程内 Bean → OpenFeign RPC
│ ├─ 通讯模型
│ │ ├─ 同步 RPC (强一致)
│ │ └─ 异步事件 (最终一致, 引用 04 篇)
│ ├─ 引用 05 篇微服务拆分策略
│ └─ 引用 02 篇六边形架构
│
├─ 全程使用: 绞杀者模式 (第 7 章)
│ ├─ 反向代理路由 (Nginx/Gateway)
│ ├─ 新老接口共存
│ ├─ 数据双写过渡
│ │ ├─ OLD_ONLY → DOUBLE_WRITE_OLD_PRIMARY
│ │ ├─ DOUBLE_WRITE_NEW_PRIMARY → NEW_ONLY
│ └─ 老接口下线四步 (Deprecated → 通知 → 强制 → 下线)
│
├─ 反模式警惕 (第 8 章)
│ ├─ 大爆炸重构 → 强制走阶段
│ ├─ 提前微服务 → Monolith First
│ ├─ 分布式单体 → 先拆库
│ └─ 数据库共享 → 一服务一库
│
├─ 全程使用: 度量与回滚 (第 9 章)
│ ├─ 四类指标 (交付/质量/技术/业务)
│ ├─ Feature Flag 动态开关
│ ├─ 灰度发布 (1% → 100%)
│ ├─ 回滚预案 (RTO ≤ 10min)
│ └─ 三级复盘 (周/月/阶段)
│
├─ 阶段 4: 微服务体系
│ ├─ 服务注册发现 (Nacos)
│ ├─ 配置中心 (Apollo/Nacos)
│ ├─ API 网关 (Gateway)
│ ├─ 限流降级 (Sentinel)
│ ├─ 链路追踪 (SkyWalking)
│ ├─ 分布式事务 (Saga, 引用 04 篇)
│ └─ 服务网格 (Istio, 可选)
│
└─ 持续演进
├─ 每季度 ADR review (引用 07 篇)
├─ 每年架构健康度评估
└─ 业务变化 → 边界重新审视 (引用 06 篇)
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
理解一次完整演进,就是理解架构不是一次性设计,是持续演进的工程实践。每一步都建立在前一步稳定的基础上,每一步都为下一步铺好路。
# 10.3 设计哲学回扣
整理本篇的四条跨阶段适用的设计哲学:
哲学 1:演进优先——架构是种出来的,不是设计出来的
最深刻的认知:没有任何架构师能在第一天就设计出未来 5 年的最优架构。业务在变、团队在变、技术在变——一次性设计的"完美架构"上线那天就过时。真正可持续的方式是让架构跟着业务一起长——单体起步、模块化打磨、数据库拆分、服务独立——每一步都是对上一步的有机生长,而不是推倒重来。这条哲学不止用于架构——延伸到任何复杂系统:产品、组织、生态,演进出来的总比设计出来的更健壮。
哲学 2:阶段为王——跳阶段等于跳悬崖
第 1 章 CTO 的核心错误不是"想拆微服务",而是"想 3 个月从阶段 0 跳到阶段 4"。每个阶段都解决一类特定问题、都需要专门验证、都需要团队适应——跳阶段意味着同时面对多个问题、没有局部修复机会、没有渐进回滚路径。架构演进的最大智慧是承认"快不了"——18 个月渐进比 3 个月大跳跃实际更快,因为不会推倒重来。这条哲学的本质:所有复杂系统的"质变"都建立在足够的"量变"之上。
哲学 3:可回滚为本——不可回滚的演进是赌博
代码可以回滚、配置可以回滚、流量可以回滚——唯独数据迁移很难回滚。这就是为什么本篇反复强调"双写过渡 6 个月"、"灰度 5%→100% 每档 1 周"、"老库保留 30 天"——给自己留后路。演进的成功率不取决于设计多漂亮,而取决于失败时能否优雅回退。这条哲学的延伸:做任何重大决策前,先想清楚"如果错了怎么回去"——回不去的不要做。
哲学 4:度量为镜——感觉好不算数,数字才算数
第 1 章 CTO 没指标——所以"3 个月完成"是空话、"架构升级了"是错觉、"我们微服务了"是自欺欺人。真正的演进必须用数字说话——部署频率、需求周期、事故率、QPS、成本——这些数字才能告诉你"演进是真的成功还是表面成功"。没有度量的演进 = 闭着眼睛开车。这条哲学的本质:任何工程实践的进步,都必须用客观指标验证;没数据支撑的"成功"都是错觉。
# 10.4 演进路径速查
一张图保存以备查:
| 阶段 | 团队 | 代码 | 数据库 | 服务 | 关键动作 | 持续时间 |
|---|---|---|---|---|---|---|
| 0 单体 | < 10 | < 10万 | 1 库 | 1 | 分层架构 | 0~12 月 |
| 1 模块化单体 | 10~30 | 10~50万 | 1 库 | 1 | 包重组+ArchUnit | 6~12 月 |
| 2 数据库拆分 | 30~50 | 50~100万 | N 库 | 1~3 | Schema 拆 + 双写 | 4~8 月 |
| 3 服务拆分 | 50~100 | 100~300万 | N 库 | 5~10 | 渐进拆 + 绞杀者 | 6~12 月 |
| 4 微服务体系 | > 100 | > 300万 | N 库 | 10~50 | 完整治理 | 长期 |
演进决策树:
当前在哪个阶段?
│
▼
现状是阶段 0?
/ \
是 否
↓ ↓
是否到临界点? 现状是阶段 1?
/ \ / \
否 是 是 否
↓ ↓ ↓ ↓
继续单体 进阶段1 是否到临界点? ...
/ \
否 是
↓ ↓
继续模块化 进阶段2
(拆数据库)
│
▼
是否拆完?
/ \
否 是
↓ ↓
继续拆 进阶段3
(拆服务)
│
▼
第一刀切哪?
(业务独立 + 收益高)
│
▼
绞杀者迁移
│
▼
灰度 5%~100%
│
▼
进阶段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
38
60 秒诊断命令清单(用于判断当前演进健康度):
# 看代码规模
find src/main -name "*.java" | xargs wc -l | tail -1
# 阈值: < 30 万行 → 阶段 0; 30 万 ~ 100 万 → 阶段 1; > 100 万 → 阶段 2+
# 看模块化程度 (跨包依赖)
jdeps -verbose:class build/libs/app.jar | grep -v "->" | wc -l
# 跨模块依赖密度 > 30% → 模块化不到位
# 看数据库共享
grep -r "@MapperScan\|@EntityScan" src/ | awk '{print $NF}' | sort -u | wc -l
# DataSource 数量 = 1 → 数据库未拆; > 1 → 阶段 2+
# 看服务数
kubectl get svc -n shop | grep -v ClusterIP | wc -l
# 1 → 单体; 2~10 → 阶段 3; > 10 → 阶段 4
# 看部署频率
git log --oneline --since="30 days ago" | grep -i "release\|deploy" | wc -l
# < 4 (周级) → 阶段 0~1; 4~20 (日级) → 阶段 2~3; > 20 (多次每日) → 阶段 4
# 看跨服务调用
grep -r "@FeignClient\|@DubboReference" src/ | wc -l
# 0 → 阶段 0~1; > 0 → 阶段 3+
# 看反模式 (分布式单体征兆)
grep -r "JOIN" src/**/mapper/**/*.xml | wc -l
# 跨服务模块的 JOIN > 0 → 警惕分布式单体
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
架构演进黄金法则:
单体优先: 新项目从单体起步,不要被"微服务焦虑"绑架
阶段稳定: 每阶段稳定 ≥ 6 个月再进下一阶段
边界先行: 先模块化再服务化, 边界没磨清楚不拆
数据先拆: 拆服务前先拆数据库,否则就是分布式单体
渐进迁移: 绞杀者模式 + 双写 + 灰度, 不要大爆炸
可回滚为本: 任何一步都能回滚, 回不去的不要做
度量驱动: 用四类指标验证演进效果, 感觉不算数
反模式警惕: 大爆炸/提前微服务/分布式单体/数据库共享
团队对齐: 康威定律—架构要与组织匹配
持续演进: 架构不是项目, 是持续 5~10 年的工程实践
红线纪律: 跳阶段 = 跳悬崖; 没回滚 = 自杀
2
3
4
5
6
7
8
9
10
11
第 1 章案例:50 人团队 3 个月拆 20 服务,210 天崩溃 → 引入"五阶段渐进 + 绞杀者 + 灰度 + 度量"四位一体 → 18 个月稳定演进到 8 个核心服务,部署提速 5 倍,QPS 提升 10 倍,P0 事故下降 80%,团队从崩溃恢复。这就是"演进 + 阶段 + 可回滚 + 度量"四位一体给团队的核心红利。
系列收官:本系列从 01.分层架构 起步,经过 02.六边形、03.CQRS、04.事件驱动、05.微服务拆分、06.DDD 战略、07.架构评审,到本篇收官——架构设计的"知"与"行"完整闭环。架构不是终点,是与业务共同生长的工程哲学。