第 16 章:单体拆分实践
第 16 章:单体拆分实践
前面的章节讲了"为什么"和"怎么做",这一章讲"怎么动手"——从第一行代码到旧系统退役的完整实战。
16.1 拆分前的准备工作
16.1.1 评估清单
拆分前必须确认的 10 件事:
□ 1. 团队具备 K8s/Docker 基本技能
□ 2. CI/CD 流水线已搭建
□ 3. 日志聚合系统就绪(ELK/Loki)
□ 4. 监控告警系统就绪(Prometheus/Grafana)
□ 5. 链路追踪系统就绪(Jaeger/Tempo)
□ 6. API 网关已部署
□ 7. 服务注册/发现机制就绪
□ 8. 数据库备份策略已制定
□ 9. 团队培训完成
□10. 管理层支持和资源保障
16.1.2 单体应用的代码分析
在拆分前,需要深入了解单体应用的内部结构:
代码分析工具:
┌──────────────────────────────────────────────────────┐
│ 单体应用分析 │
├──────────────────────────────────────────────────────┤
│ │
│ 依赖分析 │
│ ┌─────────────────────────────────────────────────┐│
│ │ 工具: jdepend / dependency-cruiser / SonarQube ││
│ │ 输出: 模块间依赖图 ││
│ │ 目标: 识别耦合度高的模块对 ││
│ └─────────────────────────────────────────────────┘│
│ │
│ 数据库分析 │
│ ┌─────────────────────────────────────────────────┐│
│ │ 工具: SchemaSpy / DBeaver ││
│ │ 输出: 表关系图、外键依赖 ││
│ │ 目标: 识别哪些表可以一起拆分 ││
│ └─────────────────────────────────────────────────┘│
│ │
│ 变更频率分析 │
│ ┌─────────────────────────────────────────────────┐│
│ │ 工具: Git log --stat / Code Climate ││
│ │ 输出: 每个模块的变更频率 ││
│ │ 目标: 高频变更的模块优先拆分 ││
│ └─────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────┘
16.1.3 依赖关系可视化
模块依赖图(理想的 vs 现实的):
理想的依赖关系(低耦合): 现实的依赖关系(高耦合):
┌──────┐ ┌──────┐ ┌──────┐──▶┌──────┐
│ 用户 │ │ 订单 │ │ 用户 │◀──│ 订单 │
└──┬───┘ └──┬───┘ └──┬───┘───└──┬───┘
│ │ │◀─────────┘│
▼ ▼ │ ┌──────┘
┌──────┐ ┌──────┐ ▼ ▼
│ 支付 │ │ 商品 │ ┌──────┐───┌──────┐
└──────┘ └──────┘ │ 支付 │◀──│ 商品 │
(单向依赖,清晰) └──────┘──▶└──────┘
(双向依赖,混乱)
16.2 绞杀者模式(Strangler Fig Pattern)实战
16.2.1 完整迁移流程
绞杀者模式 - 10 步实战流程:
Step 1: 在单体前端加一层代理
─────────────────────────────
┌────────┐ ┌──────────┐ ┌──────────┐
│ 客户端 │───▶│ Nginx │───▶│ 单体应用 │
│ │ │ (代理层) │ │ │
└────────┘ └──────────┘ └──────────┘
Step 2: 选择第一个要拆分的模块(如通知服务)
───────────────────────────────────────────
┌────────┐ ┌──────────┐ ┌──────────┐
│ 客户端 │───▶│ Nginx │───▶│ 单体应用 │
│ │ │ │───▶│ 通知服务 │ (新)
└────────┘ └──────────┘ └──────────┘
Step 3: 实现防腐层(Anti-Corruption Layer)
──────────────────────────────────────────
单体应用通过 ACL 调用新服务,而非直接修改单体代码
Step 4: 路由切换
─────────────────
将通知相关请求路由到新服务
Step 5: 验证 + 监控
─────────────────
确认新服务正常工作
Step 6-9: 重复 Step 2-5,拆分更多模块
─────────────────────────────────────
Step 10: 旧系统退役
─────────────────────
所有模块迁移完毕,下线单体应用
16.2.2 详细的路由切换策略
路由切换策略(按功能逐步迁移):
阶段 1:通知服务迁移
─────────────────────
/api/notifications/** → 通知服务 (新)
/** → 单体应用 (旧)
阶段 2:用户服务迁移
─────────────────────
/api/notifications/** → 通知服务 (新)
/api/users/** → 用户服务 (新)
/** → 单体应用 (旧)
阶段 3:商品服务迁移
─────────────────────
/api/notifications/** → 通知服务 (新)
/api/users/** → 用户服务 (新)
/api/products/** → 商品服务 (新)
/** → 单体应用 (旧)
... 逐步迁移 ...
阶段 N:全部迁移完成
─────────────────────
/api/notifications/** → 通知服务
/api/users/** → 用户服务
/api/products/** → 商品服务
/api/orders/** → 订单服务
/api/payments/** → 支付服务
(单体应用已退役)
16.2.3 Nginx 路由配置示例
# nginx.conf
upstream monolith {
server monolith-app:8080;
}
upstream notification_service {
server notification-service:8080;
}
upstream user_service {
server user-service:8080;
}
upstream order_service {
server order-service:8080;
}
server {
listen 80;
# 已迁移:通知服务
location /api/v1/notifications/ {
proxy_pass http://notification_service;
proxy_set_header X-Request-ID $request_id;
}
# 已迁移:用户服务
location /api/v1/users/ {
proxy_pass http://user_service;
proxy_set_header X-Request-ID $request_id;
}
# 已迁移:订单服务
location /api/v1/orders/ {
proxy_pass http://order_service;
proxy_set_header X-Request-ID $request_id;
}
# 未迁移:走单体
location / {
proxy_pass http://monolith;
proxy_set_header X-Request-ID $request_id;
}
}
16.3 防腐层(Anti-Corruption Layer)
16.3.1 为什么需要防腐层
问题:新服务的模型与旧系统不兼容
旧系统(单体): 新服务(微服务):
┌─────────────────┐ ┌─────────────────┐
│ class TblUser { │ │ class UserDTO { │
│ f_id │ │ userId │
│ f_name │ │ fullName │
│ f_sex │ (0/1) │ gender │ (MALE/FEMALE)
│ f_addr │ │ address { │
│ } │ │ city, ... │
│ │ │ } │
│ │ │ } │
└─────────────────┘ └─────────────────┘
防腐层的作用:在两个模型之间做翻译
16.3.2 ACL 实现
// 防腐层:将旧系统的模型转换为新服务的模型
@Service
public class UserAntiCorruptionLayer {
private final UserGrpcClient userGrpcClient;
// 旧系统调用新服务的适配器
public TblUser getUserForLegacySystem(String userId) {
UserDTO newUser = userGrpcClient.getUser(userId);
return convertToLegacy(newUser);
}
// 新服务需要旧系统数据时的适配器
public UserDTO getUserForNewService(String legacyUserId) {
// 从旧系统获取数据并转换
TblUser legacyUser = legacyRepository.findById(legacyUserId);
return convertFromLegacy(legacyUser);
}
private TblUser convertToLegacy(UserDTO dto) {
TblUser entity = new TblUser();
entity.setF_id(dto.getUserId());
entity.setF_name(dto.getFullName());
entity.setF_sex("MALE".equals(dto.getGender()) ? "1" : "0");
entity.setF_addr(dto.getAddress().getCity() + " " +
dto.getAddress().getDetail());
return entity;
}
private UserDTO convertFromLegacy(TblUser entity) {
UserDTO dto = new UserDTO();
dto.setUserId(entity.getF_id());
dto.setFullName(entity.getF_name());
dto.setGender("1".equals(entity.getF_sex()) ? "MALE" : "FEMALE");
AddressDTO address = parseAddress(entity.getF_addr());
dto.setAddress(address);
return dto;
}
}
16.3.3 ACL 设计模式
ACL 常见模式:
1. 适配器模式 (Adapter)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 旧系统 │───▶│ 适配器 │───▶│ 新服务 │
│ (调用方) │ │ (ACL) │ │ (被调用方)│
└──────────┘ └──────────┘ └──────────┘
2. 外观模式 (Facade)
┌──────────┐ ┌──────────┐
│ 新服务 │───▶│ 外观 │───▶ 多个旧系统接口
│ (调用方) │ │ (ACL) │ ┌──────┐
└──────────┘ └──────────┘ │旧API1│
│旧API2│
│旧API3│
└──────┘
3. 网关模式 (Gateway)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 外部系统 │───▶│ 网关 │───▶│ 内部服务 │
│ (第三方) │ │ (ACL) │ │ │
└──────────┘ └──────────┘ └──────────┘
16.4 数据迁移策略
16.4.1 双写策略
双写过渡期:
┌──────────────────────────────────────────────┐
│ │
│ 读写分离期(过渡) │
│ ┌──────────────────────────────────────────┐│
│ │ 应用层 ││
│ │ 写操作 → 旧库 (主) + 新库 (同步) ││
│ │ 读操作 → 旧库 ││
│ └──────────────────────────────────────────┘│
│ │
│ ┌──────────────────────────────────────────┐│
│ │ 切换读路径 ││
│ │ 写操作 → 旧库 (主) + 新库 (同步) ││
│ │ 读操作 → 新库 ││
│ └──────────────────────────────────────────┘│
│ │
│ ┌──────────────────────────────────────────┐│
│ │ 完全切换 ││
│ │ 写操作 → 新库 (主) ││
│ │ 读操作 → 新库 ││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
16.4.2 数据一致性校验
// 数据一致性校验任务
@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟执行
public void validateDataConsistency() {
// 获取旧库和新库的最近变更数据
List<OldUser> oldUsers = oldRepo.findRecentlyUpdated(lastCheckTime);
List<NewUser> newUsers = newRepo.findRecentlyUpdated(lastCheckTime);
// 对比数据
for (OldUser oldUser : oldUsers) {
NewUser newUser = newUsers.stream()
.filter(u -> u.getOldId().equals(oldUser.getId()))
.findFirst()
.orElse(null);
if (newUser == null) {
log.error("数据不一致:旧库存在但新库不存在 {}", oldUser.getId());
alertService.send("DATA_INCONSISTENCY", oldUser.getId());
} else if (!isConsistent(oldUser, newUser)) {
log.error("数据不一致:字段差异 {}", oldUser.getId());
alertService.send("DATA_MISMATCH", oldUser.getId());
}
}
lastCheckTime = Instant.now();
}
16.5 模块化单体作为过渡方案
16.5.1 模块化单体架构
模块化单体(Modular Monolith):
┌──────────────────────────────────────────────────────┐
│ 模块化单体应用 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 公共层 (Common) │ │
│ │ 共享工具、配置、安全框架 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户模块 │ │ 订单模块 │ │ 商品模块 │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │API │ │ │ │API │ │ │ │API │ │ │
│ │ │Service│ │ │ │Service│ │ │ │Service│ │ │
│ │ │Domain │ │ │ │Domain │ │ │ │Domain │ │ │
│ │ │Repo │ │ │ │Repo │ │ │ │Repo │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 模块间通过接口(Interface)通信,不直接访问内部类 │
└──────────────────────────────────────────────────────┘
16.5.2 Spring Modulith 示例
// 模块间通过 Application Event 通信
@Service
public class OrderService {
@ApplicationEventPublisher
private ApplicationEventPublisher events;
@Transactional
public Order createOrder(CreateOrderCommand command) {
Order order = new Order(command);
orderRepository.save(order);
// 发布领域事件(模块间通信)
events.publishEvent(new OrderCreatedEvent(order.getId(),
order.getCustomerId(), order.getTotalAmount()));
return order;
}
}
// 其他模块监听事件
@Component
public class InventoryEventHandler {
@TransactionalEventListener
public void onOrderCreated(OrderCreatedEvent event) {
inventoryService.deductStock(event.getItems());
}
}
16.6 业务场景:完整迁移案例
16.6.1 背景
某企业级 CRM 系统,运行 8 年,Java 单体应用,代码 150 万行,20 人团队。
16.6.2 迁移时间线
┌──────────────────────────────────────────────────────────────┐
│ CRM 系统迁移时间线 (12 个月) │
├──────────────────────────────────────────────────────────────┤
│ │
│ Month 1-2: 准备阶段 │
│ ├── 代码分析、模块依赖梳理 │
│ ├── 搭建 K8s 集群 + CI/CD + 监控 │
│ ├── 团队培训 │
│ └── 确定拆分顺序 │
│ │
│ Month 3-4: 试点阶段 │
│ ├── 拆分通知服务(低风险模块) │
│ ├── 拆分文件服务(低风险模块) │
│ └── 验证整套流程可行 │
│ │
│ Month 5-6: 核心模块拆分 (1) │
│ ├── 拆分用户管理服务 │
│ ├── 拆分客户管理服务 │
│ └── 数据库 Schema 分离 │
│ │
│ Month 7-8: 核心模块拆分 (2) │
│ ├── 拆分销售机会服务 │
│ ├── 拆分合同管理服务 │
│ └── 物理数据库分离 │
│ │
│ Month 9-10: 核心模块拆分 (3) │
│ ├── 拆分报表服务 │
│ ├── 拆分营销自动化服务 │
│ └── 实施 CQRS(读写分离) │
│ │
│ Month 11-12: 收尾阶段 │
│ ├── 旧系统下线 │
│ ├── 性能优化 │
│ └── 文档更新、复盘 │
└──────────────────────────────────────────────────────────────┘
⚠️ 注意事项
- 不要大爆炸重写——渐进式迁移是唯一可靠的方式
- 保持旧系统可运行——在新系统完全验证前,旧系统必须保持运行
- 数据迁移先行——数据拆分是最难的部分,提前规划
- 自动化测试覆盖——迁移前确保有足够的测试覆盖
- 监控先行——先有监控,再做拆分,否则出问题无法定位
📖 扩展阅读
- Martin Fowler - StranglerFigApplication — 绞杀者模式原始描述
- Sam Newman - Monolith to Microservices — 单体到微服务的完整指南
- Spring Modulith — 模块化单体框架
- Eric Evans - Anti-Corruption Layer — 防腐层设计模式
- Building Evolutionary Architectures — 演进式架构
本章小结
| 策略 | 适用场景 | 关键要点 |
|---|---|---|
| 绞杀者模式 | 大型遗留系统 | 逐步替换,保持旧系统运行 |
| 防腐层 | 新旧系统交互 | 模型翻译,防止污染 |
| 双写策略 | 数据迁移 | 保证数据不丢失 |
| 模块化单体 | 过渡方案 | 先在单体内做好边界划分 |
📌 下一章:第 17 章:故障排查 — 分布式调试、性能排查、常见问题解决。