强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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: 收尾阶段                                       │
  │  ├── 旧系统下线                                              │
  │  ├── 性能优化                                                │
  │  └── 文档更新、复盘                                          │
  └──────────────────────────────────────────────────────────────┘

⚠️ 注意事项

  1. 不要大爆炸重写——渐进式迁移是唯一可靠的方式
  2. 保持旧系统可运行——在新系统完全验证前,旧系统必须保持运行
  3. 数据迁移先行——数据拆分是最难的部分,提前规划
  4. 自动化测试覆盖——迁移前确保有足够的测试覆盖
  5. 监控先行——先有监控,再做拆分,否则出问题无法定位

📖 扩展阅读

  1. Martin Fowler - StranglerFigApplication — 绞杀者模式原始描述
  2. Sam Newman - Monolith to Microservices — 单体到微服务的完整指南
  3. Spring Modulith — 模块化单体框架
  4. Eric Evans - Anti-Corruption Layer — 防腐层设计模式
  5. Building Evolutionary Architectures — 演进式架构

本章小结

策略适用场景关键要点
绞杀者模式大型遗留系统逐步替换,保持旧系统运行
防腐层新旧系统交互模型翻译,防止污染
双写策略数据迁移保证数据不丢失
模块化单体过渡方案先在单体内做好边界划分

📌 下一章第 17 章:故障排查 — 分布式调试、性能排查、常见问题解决。