编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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 分层单体代码
        • 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 RPC与事件协同
        • 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-12-03
目录

架构演进实战指南

# 08.架构演进实战指南

# 目录介绍

  • 1. 案例引入
    • 1.1 一次失败的大爆炸重构
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 演进全景图
    • 2.1 五阶段总图
    • 2.2 为什么这么切
  • 3. 单体起步阶段
    • 3.1 单体的合理性
    • 3.2 分层单体代码
    • 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 RPC与事件协同
    • 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 一次失败的大爆炸重构

先看一段在某 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 引咎辞职
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

直觉怀疑:是不是技术选型有问题?

查 GitHub:
  - Nacos:    阿里出品, 业界标准              ✅ 没问题
  - Sentinel: 限流降级, 业界标准              ✅ 没问题
  - SkyWalking: APM 标杆                    ✅ 没问题
  - Seata:    分布式事务                    ✅ 没问题
1
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 章
1
2
3
4
5
6
7
8

# 1.3 我们要回答什么

这个事故就是本篇的主线案例。我们带着上面 8 个问号往下走,每讲完一段方法论就解开一两个;最后在第 10 章把案例彻底剖开,并给出"如果重新来一次该怎么做"的渐进式方案。

本篇路线:

演进总图 (第 2 章)
   ↓
单体起步 (第 3 章) ─→ 解开"为什么单体不丢人"
   ↓
模块化单体 → 数据库拆分 → 服务拆分 (第 4-6 章) ─→ 解开"三步走的演进路径"
   ↓
绞杀者 → 反模式 (第 7-8 章) ─→ 解开"老系统平滑迁移与四大病"
   ↓
度量与回滚 (第 9 章) ─→ 武器库
   ↓
综合案例 (第 10 章) ─→ 案例彻底剖开
1
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
                        多库少服务            多库多服务
1
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: 引入服务网格、配置中心、链路追踪、限流降级
1
2
3
4

# 2.2 为什么这么切

为什么把演进切成"五阶段渐进",而不是"一步到位拆微服务"?

疑惑:第 1 章 CTO 是不是直接从阶段 0 跳到阶段 4,所以才挂的?

论证:

  1. 每阶段解决的问题不同——阶段 1 解决"代码乱"(边界),阶段 2 解决"数据耦合"(共享库),阶段 3 解决"部署耦合"(独立发布),阶段 4 解决"治理混乱"(服务网格)。跳阶段 = 同时解多个问题 = 没一个能解好。

  2. 每阶段验证学习不同——阶段 1 验证"边界是否合理",阶段 2 验证"跨库查询能否承受",阶段 3 验证"RPC 延迟与故障容忍"。跳阶段 = 没机会试错 = 错了无法局部修复。

  3. 每阶段成本递增——阶段 1 几乎 0 基础设施投入,阶段 4 要 Nacos/Sentinel/SkyWalking/Seata/K8s 全套。跳阶段 = 团队还没准备好就背巨额成本。

  4. 每阶段回滚代价不同——阶段 1 改包结构错了改回去 1 周,阶段 4 数据拆完了回不去半年。渐进式 = 每步可回滚 = 风险可控。

  5. 反向验证:如果不分阶段会怎样?参考第 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,先活下来再说
1
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 即可                       │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8

适用判定:

当满足以下任一条件 → 单体优先:
  ☑ 团队 < 10 人
  ☑ 业务边界未稳定 (产品频繁变方向)
  ☑ MVP 阶段, 6 个月内可能砍方向
  ☑ 单机能扛得住 (QPS < 1000、数据 < 100 GB)
  ☑ 团队无微服务运维经验

不应该单体优先 → 直接上模块化甚至更高:
  ☐ 业务边界极其清晰 (如金融账户、电商下单)
  ☐ 团队规模 > 30 人, 必须并行开发
  ☐ 业务量已大 (QPS > 10000)
1
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 张表)             │     │
│  └─────────────────────────────────────┘     │
└──────────────────────────────────────────────┘
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

典型代码(极简版):

// 包结构 (按"技术分层"组织)
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));
    }
}
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

这就是阶段 0 的典型样子——按技术分层组织、业务模块通过 @Autowired 直接调用、共享同一个事务、共享同一个数据库。

# 3.3 单体的临界点

疑惑:单体什么时候该升级?

论证:观察四个信号——任何一个出现,就该考虑升级到模块化单体:

信号 1: 启动慢 (> 60 秒)
  根因: 类太多、Spring 扫描慢、Bean 数 > 1000

信号 2: 部署慢 (> 20 分钟)
  根因: 构建慢、测试慢、回滚慢

信号 3: 改 A 挂 B
  根因: 模块边界模糊, 共用 Service 牵一发动全身

信号 4: 团队抢代码
  根因: 5+ 个团队改同一个 Service.java, merge 冲突天天有
1
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 微服务"
1
2
3
4
5
6
7
8
9

# 3.4 演进前的准备

疑惑:升级前要做什么准备?

论证:演进 ≠ 直接动手——需要先做四件准备工作:

准备 1: 业务梳理 (1~2 周)
  - 列出所有业务能力 (Capabilities)
  - 识别核心域 / 支撑域 / 通用域 (参见 06.DDD)
  - 找出业务边界冲突点

准备 2: 技术现状评估 (1 周)
  - 代码扫描: 类 / 接口 / 包依赖关系
  - 数据库扫描: 表 / 外键 / 跨业务 JOIN
  - 调用链路: 哪些方法跨"业务模块"调用

准备 3: 团队能力评估 (1 周)
  - 团队规模与组织结构 (康威定律)
  - 微服务运维经验
  - DevOps 基础设施现状

准备 4: 风险与回滚预案 (1 周)
  - 演进各阶段的回滚成本
  - 业务方对停机的容忍度
  - 关键人是否到位
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
1
2
3
4
5
6
7
8
9
10
11

结论:不做准备就开始演进 = 第 1 章悲剧重演。准备的目的是回答两个问题——"现在在哪里"、"下一步去哪里"。任何一个答不清楚,就不要动手。

# 4. 模块化单体阶段

# 4.1 模块化的核心

疑惑:模块化单体和普通单体有什么区别?不就是改改包名吗?

论证:模块化单体是单体的进化形态——保留单体的部署简单,但通过强制边界解决"改 A 挂 B"。

核心思想:

普通单体:  按"技术分层"组织, 跨模块随意 import
模块化单体: 按"业务能力"组织, 模块间禁止跨边界 import
1
2

对比图:

普通单体 (按技术分层):              模块化单体 (按业务能力):
┌─────────────────────┐           ┌─────────────────────┐
│   controller        │           │ order   product user │
├─────────────────────┤           │  ┌─┐    ┌─┐   ┌─┐   │
│   service           │           │  │c│    │c│   │c│   │
├─────────────────────┤           │  │s│    │s│   │s│   │
│   repository        │           │  │r│    │r│   │r│   │
└─────────────────────┘           │  └─┘    └─┘   └─┘   │
                                  └─────────────────────┘
跨模块直接调用 ← 边界模糊            模块间通过 API 调用 ← 边界清晰
1
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
1
2
3
4
5
6
7
8
9
10
11

# 4.2 包结构重构

重构前(按技术分层):

com.shop
├── controller        ← 所有 Controller 混一起
├── service           ← 所有 Service 混一起
├── repository        ← 所有 Repository 混一起
└── entity            ← 所有 Entity 混一起
1
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
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

关键代码示范:

// 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
        // ...
    }
}
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
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();
}
1
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 表
  ❌ 严禁! 这就是"模块化失败"的起点
1
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());
    }
}
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

关键:进程内事件已经为未来拆分微服务铺好了路——当模块独立成服务时,事件从进程内换成 Kafka 即可,业务代码几乎不变。

# 4.4 模块化的边界

疑惑:模块怎么划?划多大?

论证:模块化的边界 = 限界上下文(参见 [06.DDD])。判定四标尺:

标尺 1: 业务能力独立性
  - 这个模块对应一个独立的业务能力 (订单/商品/用户)
  - 该能力可以被业务方单独描述、单独验收

标尺 2: 数据所有权
  - 这个模块拥有一组自己的表
  - 其他模块不直接读写这些表

标尺 3: 团队归属
  - 这个模块由一个团队 (2~10 人) 负责
  - 一人不同时归属多个模块

标尺 4: 变更频率
  - 这个模块的变更与其他模块解耦
  - 80% 的需求只改一个模块
1
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:   还是普通单体, 别自欺欺人
1
2
3
4
5
6
7
8
9
10
11

红线:没经过模块化阶段就直接拆服务,等同于在烂泥地上盖楼——边界都没磨清楚,进程拆开只会更乱。模块化是微服务的预演——演练好了再上正式舞台。

# 5. 数据库拆分阶段

# 5.1 单库的瓶颈

疑惑:模块化都做好了,数据库一直共享不行吗?

论证:试试看共享单库的后果:

共享单库的四大问题:

问题 1: 模块间数据耦合
  - 订单模块改 product 表加列 → 商品模块编译失败
  - 数据库表 = 全局耦合点

问题 2: 性能瓶颈无法局部解决
  - 商品大促压力大 → 整个库 CPU 飙升
  - 订单模块也跟着挂

问题 3: 故障爆炸半径大
  - 某个慢 SQL 把连接池打满
  - 所有模块都挂

问题 4: 无法独立扩容
  - 想给商品加只读副本 → 必须给整个库加
  - 资源利用率低
1
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 个团队
1
2
3
4

结论:数据库不拆,模块化只是表象。微服务的本质是 "独立部署、独立伸缩、独立故障"——而独立故障的前提是独立数据。共享数据库的"微服务"叫分布式单体(参见 8.3)。

# 5.2 一库一模块改造

核心原则:一个模块一个 Schema/数据库。

改造前(共享单库):

┌─────────────────────────────────────────┐
│         shop_db (单库)                  │
│  ┌──────────┐ ┌──────────┐ ┌─────────┐│
│  │ t_order  │ │ t_product│ │ t_user  ││
│  │ t_pay    │ │ t_sku    │ │ t_addr  ││
│  └──────────┘ └──────────┘ └─────────┘│
└─────────────────────────────────────────┘
        ▲           ▲           ▲
        └───────────┼───────────┘
              所有模块共享
1
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   │
└────▲─────┘      └────▲─────┘      └────▲─────┘
     │                 │                 │
┌────┴─────┐      ┌────┴─────┐      ┌────┴─────┐
│ 订单模块  │      │ 商品模块  │      │ 用户模块  │
└──────────┘      └──────────┘      └──────────┘
1
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 一台)
  - 监控独立、备份独立、扩容独立
1
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);
    }
}
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
# 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}
1
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 跑不动了 → 怎么办?
1
2
3
4
5
6
7
8

四方案对比:

┌──────────────────────────────────────────────────────────┐
│  方案              适用场景              代价              │
├──────────────────────────────────────────────────────────┤
│ 1. 应用层 JOIN     低频查询、小数据集    多次 IO          │
│ 2. 数据冗余        热点字段、写少读多    一致性维护        │
│ 3. CQRS 读模型     复杂报表、列表查询    最终一致          │
│ 4. 数据复制        实时跨库查询          全套同步组件      │
└──────────────────────────────────────────────────────────┘
1
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 × 单次延迟
1
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),        -- ← 冗余字段
    ...
);

-- 适用: 字段稳定 (名字、品类)、读多写少
-- 缺点: 商品改名后,订单里的名字怎么办? (策略: 不变,记录下单时的名字)
1
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 = ?
适用: 复杂报表、多维筛选
代价: 增加投影组件 + 最终一致
1
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)
1
2
3
4
5

选型决策树:

跨库查询需求来了
        │
        ▼
是否高频 (>100 QPS)?
   /            \
  否            是
   ↓            ↓
应用层 JOIN  是否复杂报表?
   (方案 1)    /         \
              否          是
              ↓           ↓
        是否字段稳定?    CQRS 读模型
        /          \      (方案 3)
       是          否
       ↓           ↓
   数据冗余     数据复制
   (方案 2)    (方案 4)
1
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 天,确认无问题再删
1
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
1
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: 团队成熟度
  - 有专门团队负责该模块
  - 团队具备独立运维能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

优先级矩阵:

                  业务独立性
                       ▲
                       │
            高 │  优先拆     重点拆     战略拆
                │ (易+收益高) (核心)    (难+收益高)
                │
            中 │  顺手拆     慎重拆     不建议
                │
            低 │  不拆       不拆       不拆
                │
                └──────────────────────────► 收益 (变更/性能)
                    低         中         高
1
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 个月,必然崩
1
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 已抽出)
1
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 改动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这就是第 4 章"模块化时坚持 API 接口"的核心价值——拆服务时业务代码 0 改动,只改注解 + 部署。

新增的"分布式问题":

原来本地调用                  现在 RPC 调用
─────────────────────────────────────────────────
延迟 ~ns                  →   延迟 ~ms (1000x)
不会失败                  →   网络可能超时/抖动
强一致事务                →   分布式事务问题
本地异常                  →   远程异常 (序列化)
直接 debug                →   分布式追踪 (Tracing)
1
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
1
2
3
4
5
6

# 6.3 RPC与事件协同

拆服务后,通讯模型有两种:

模型 1: 同步 RPC (强一致需求)
  下单 → 调用 ProductService.deductStock() → 等返回 → 写订单
  优点: 立即知道扣减结果
  缺点: 强耦合, 商品挂了订单也挂

模型 2: 异步事件 (最终一致需求)
  下单 → 写订单 → 发 OrderCreated 事件
  ProductService 消费事件 → 异步扣库存
  优点: 解耦, 商品慢/挂不影响下单
  缺点: 库存可能短暂不一致
1
2
3
4
5
6
7
8
9
10

选型原则:

强一致场景 (扣款/扣库存):
  → 同步 RPC + 分布式事务 (Saga)

最终一致场景 (积分/通知/统计):
  → 异步事件 (Kafka + Outbox)

查询场景:
  → 同步 RPC (短超时 + 兜底)
1
2
3
4
5
6
7
8

典型混合架构:

┌──────────┐   RPC    ┌──────────┐
│ 订单服务  │ ───────► │ 商品服务  │  ← 同步扣库存
└────┬─────┘          └──────────┘
     │
     │ Event
     ▼
┌──────────┐  Consume  ┌──────────┐
│  Kafka   │ ────────► │ 积分服务  │  ← 异步加积分
└──────────┘           └──────────┘
                       ┌──────────┐
                       │ 通知服务  │  ← 异步发短信
                       └──────────┘
1
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 天紧急回滚)              │
└───────────────────────────────────────────────────┘
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

影子流量代码示例:

@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;            // 返回老结果给用户
    }
}
1
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 年, 业务中断, 公司倒闭
1
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: 老系统完全下线
1
2
3
4
5
6
7
8
9
10

核心思想:新老共存、逐步迁移、随时回滚。

┌──────────────────────────────────────────────┐
│           反向代理 / API 网关                  │
│  (路由规则: /new/* → 新, /old/* → 老)          │
└────────────────┬─────────────────────────────┘
                 │
        ┌────────┴────────┐
        ▼                 ▼
┌──────────────┐   ┌──────────────┐
│  新系统       │   │  老系统       │
│  (渐进开发)   │   │  (持续运行)   │
└──────────────┘   └──────────────┘
        │                 │
        └────────┬────────┘
                 ▼
        ┌──────────────┐
        │   数据库      │ ← 共享 或 双写
        └──────────────┘
1
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;
    }
}
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

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
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

# 7.3 老接口下线节奏

绞杀的节奏要稳——不能太快也不能太慢:

节奏 1: 太快 (1 个月切完)
  风险: 没观察期, 老业务找不到归属
  后果: 不一致问题集中爆发

节奏 2: 太慢 (3 年还在迁)
  风险: 双跑成本叠加, 团队疲惫
  后果: 项目烂尾, 老系统永生

节奏 3: 适中 (6~18 个月)
  推荐: 每月迁 10~20% 接口
  关键: 观察 + 复盘 + 下个接口
1
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 个月
  - 真正删除代码
1
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
1
2
3
4

# 7.4 数据双写过渡

最复杂的一步——数据从老库迁到新库,过渡期需要双写:

┌──────────────────────────────────────────────┐
│             应用层 (双写逻辑)                  │
└───────┬────────────────────────┬─────────────┘
        │ 写老库 (主)              │ 写新库 (影子)
        ▼                        ▼
┌──────────────┐          ┌──────────────┐
│  老数据库     │          │  新数据库     │
└──────────────┘          └──────────────┘
        ↑                        ↑
        └──── Canal 同步 ────────┘
              (校验一致性)
1
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));
        }
    }
}
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

双写的四阶段切换:

阶段          模式                       读流量         写流量
────────────────────────────────────────────────────────────
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% 新
1
2
3
4
5
6
7

关键纪律:

1. 双写期间, 一致性校验工具必须 7×24 运行
2. 不一致超过阈值 (>0.1%) 立刻告警 + 暂停切流
3. 每阶段持续 ≥ 2 周, 不允许跳阶段
4. 每阶段保留回滚路径 (切回上一阶段)
1
2
3
4

# 8. 演进反模式

# 8.1 大爆炸重构

症状(即第 1 章案例):

- "我们 3 个月把单体改成 20 个微服务"
- "不留兼容代码, 直接切"
- "上线那天全员加班守一晚就行"
1
2
3

根因:

  • 把"架构演进"当成"项目交付"
  • 没有阶段验证, 错了无法局部修正
  • 数据库一拆, 回滚成本指数级上升

修复:

正确姿势:
1. 拒绝"3 个月拆完"的承诺
2. 强制走阶段: 单体 → 模块化 → 数据库拆 → 服务拆
3. 每阶段稳定 ≥ 6 个月再进下一阶段
4. 每一步都有"灰度 + 双跑 + 回滚"三件套
5. 演进周期至少 12~24 个月,接受现实
1
2
3
4
5
6

红线:任何"3 个月完成大重构"的承诺都是骗局——大重构是 1~2 年的事, 接受这个现实才能成功。

# 8.2 提前微服务

症状:

5 人创业团队, 第一天就拆 10 个微服务
理由: "听说微服务是未来"
结果: 90% 时间在搞基础设施, MVP 永远出不来
1
2
3

根因:

  • 把"业界最佳实践"误以为"放之四海皆准"
  • 不理解微服务的成本曲线
  • 不理解"边界发现"需要时间

修复:

判定原则: "Monolith First"
1. 团队 < 10 人 → 单体起步
2. 业务边界未稳定 → 单体起步
3. 没有 DevOps 基础 → 单体起步
4. 团队从未运维过微服务 → 单体起步

正确节奏:
- 0~6 月: 单体跑通 MVP
- 6~12 月: 重构成模块化单体
- 12~24 月: 拆 1~2 个核心服务
- 24~36 月: 完整微服务体系
1
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
1
2
3
4
5

这就是"分布式单体"——有微服务的成本, 没有微服务的红利。

根因:

  • 拆服务时只切代码, 没切数据
  • 拆服务时没拆团队 (康威定律反作用)
  • 跨服务调用泛滥 (边界没找对)

修复:

诊断指标:
☐ 一次需求改 > 3 个服务 → 分布式单体
☐ 一次请求 > 5 次 RPC → 分布式单体
☐ 任何服务挂导致全链路挂 → 分布式单体
☐ 多服务共享数据库 → 分布式单体

修复方案:
1. 数据先拆 (一库一服务)
2. 重新审视边界 (限界上下文)
3. 高频协同的服务合并回去 (是的, 合并是允许的!)
4. 异步事件替换大量 RPC
5. 团队结构调整 (一服务一团队)
1
2
3
4
5
6
7
8
9
10
11
12

反思:分布式单体比单体更糟——单体至少简单, 分布式单体既复杂又脆弱。宁可回到单体也别留在分布式单体。

# 8.4 数据库共享

症状:

拆了服务但库没拆
理由: "拆库太麻烦,先这样"
结果:
- A 服务改表结构 → B 服务编译失败
- 跨服务 JOIN 满天飞
- 想给 B 加只读副本 → 必须给整个库加
- 服务"独立"是个笑话
1
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 个月内物理拆库计划
1
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)          目标: 提升         │
│     - 资源成本 (服务器/月)        目标: 优化         │
│                                                      │
└──────────────────────────────────────────────────────┘
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

第 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

→ 实际拆完所有指标恶化, 这就是"失败的演进"
→ 没有指标体系, 团队还在自欺欺人"我们升级了"
1
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% 切换
1
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 后)
1
2
3
4
5

灰度的黄金法则:

1. 先内部, 后外部 (员工 → 灰度用户 → 全量)
2. 先少量, 后大量 (1% → 5% → 25% → 50% → 100%)
3. 先低风险, 后高风险 (查询接口 → 写入接口 → 支付接口)
4. 每档观察 ≥ 24 小时
5. 任何指标异常立刻回滚 (开关一关即回滚)
1
2
3
4
5

# 9.3 回滚预案

最重要的一节——没回滚预案就不算上线。

回滚预案四要素:

要素 1: 回滚触发条件 (Trigger)
  - P0 事故出现
  - 核心指标偏离 > X%
  - 灰度组反馈 > N 起严重问题

要素 2: 回滚执行步骤 (Steps)
  - 谁来按按钮 (Owner)
  - 按哪个按钮 (具体操作)
  - 多久能回滚完 (RTO)

要素 3: 回滚验证 (Verify)
  - 回滚后核心指标是否恢复
  - 数据是否一致
  - 用户是否感知

要素 4: 回滚后续 (Postmortem)
  - 立刻召集复盘会
  - 根因分析
  - 下次重试的准入条件
1
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 频道
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

# 9.4 复盘机制

演进过程必须定期复盘——否则错误会重复。

三种复盘节奏:

节奏 1: 每周 Standup (15 分钟)
  - 本周演进进展
  - 遇到的阻塞
  - 下周计划

节奏 2: 每月里程碑复盘 (1 小时)
  - 本月指标变化
  - 计划 vs 实际偏差
  - 下月调整

节奏 3: 阶段性复盘 (半天)
  - 阶段目标达成度
  - 关键决策回顾 (ADR review)
  - 是否进入下一阶段
1
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 服务拆分"
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

复盘的"红线":

1. 失败不甩锅 (对事不对人)
2. 行动项必须有 Owner + Deadline
3. 行动项必须下次复盘核查
4. 不要重复犯同一个错 (超过 2 次升级到流程治理)
1
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%
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

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

方案 A · 立刻停止当前拆分(最紧迫)

立刻动作:
  - 暂停剩余拆分计划
  - 已拆服务: 合并回 5~8 个核心服务
  - 数据库回归: 一服务一库, 砍掉"分布式单体"
  - 引入灰度 + 双跑

代价: 短期看着"倒退",但能止损
收益: 团队从崩溃恢复, 重新出发
1
2
3
4
5
6
7
8

方案 B · 回退到阶段 1 重新出发(中等代价)

立刻动作:
  - 把 20 个服务合并回模块化单体
  - 引入 ArchUnit 重塑边界
  - 数据库逐步收回单库 (Schema 隔离)
  - 重新走渐进路径

代价: 半年回退期
收益: 走对路径, 后续可控
1
2
3
4
5
6
7
8

方案 C · 引入外部专家全面诊断(最重)

立刻动作:
  - 聘请架构顾问 (2~3 周诊断)
  - 全面评估当前状态
  - 给出 12 个月恢复 + 演进路径
  - 团队培训 + 流程重建

代价: 顾问费 + 团队重建时间
收益: 系统性恢复, 避免再犯
1
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 篇)
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
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 (微服务体系)
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
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 → 警惕分布式单体
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

架构演进黄金法则:

单体优先:     新项目从单体起步,不要被"微服务焦虑"绑架
阶段稳定:     每阶段稳定 ≥ 6 个月再进下一阶段
边界先行:     先模块化再服务化, 边界没磨清楚不拆
数据先拆:     拆服务前先拆数据库,否则就是分布式单体
渐进迁移:     绞杀者模式 + 双写 + 灰度, 不要大爆炸
可回滚为本:   任何一步都能回滚, 回不去的不要做
度量驱动:     用四类指标验证演进效果, 感觉不算数
反模式警惕:   大爆炸/提前微服务/分布式单体/数据库共享
团队对齐:     康威定律—架构要与组织匹配
持续演进:     架构不是项目, 是持续 5~10 年的工程实践
红线纪律:     跳阶段 = 跳悬崖; 没回滚 = 自杀
1
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.架构评审,到本篇收官——架构设计的"知"与"行"完整闭环。架构不是终点,是与业务共同生长的工程哲学。

#架构#演进
上次更新: 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
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式