单体系统怎样分步拆成服务
💡 单体巨石架构的生产困境
在企业级应用演进的初期,单体架构(Monolith)凭其简单的部署和极高的开发效率支撑了业务的快速跑通。然而,当单体 Spring Boot 工程代码量突破 60 万行、数据库包含 300 多张表、研发团队扩充到上百人时,单体架构的噩梦便接踵而至。
上个月,某核心电商系统的日常发布演练再次引发事故。由于用户模块、订单模块和支付模块的代码深度交织在同一个 JVM 进程中,开发人员修改了一行用户积分计算逻辑,意外引发了订单事务回滚机制失效,导致生产环境产生了数百笔数据不一致的“幽灵订单”。
面对巨石系统,盲目推翻重来(Big Bang Rewrite)的失败率高达 90%。唯一的破局之道是采用绞杀者模式 (Strangler Fig Pattern):在维持老系统正常运行的同时,像绞杀植物生长一样,在边缘逐步构建新的微服务,将流量一点点剥离出来,直至旧系统被完全替换。
一、 绞杀者模式落地演进路线与数据防线
服务拆分不仅仅是代码文件的迁移,本质上是数据存储边界与事务控制权的重新划定。
1. 拆分边界识别:DDD 限界上下文划定
拆分的第一步不是动代码,而是按领域驱动设计 (DDD) 寻找切入点。优先选择业务变更频繁、对 CPU/内存消耗有特殊需求、以及代码依赖耦合度较低的模块(如支付结算或订单履约模块)作为首个绞杀目标。
2. 流量透明切流与双向镜像比对 (Traffic Shadowing)
在网关层(Spring Cloud Gateway)基于请求 Header 或用户 ID 设置动态切流规则。在初期,将生产流量复制一份(Shadow Traffic)同时打入新微服务和旧单体模块,比对两者的输出结果与数据库落盘差异,确保逻辑 100% 兼容后再真正切流。
3. 数据一致性防线:Transactional Outbox 模式
拆分后,原本依靠单体数据库本地事务(@Transactional)保障的一致性被打破。避免使用两阶段提交(2PC/Seata AT)这种重型强一致性方案拖垮吞吐量。生产环境应采用基于本地消息表与 MQ 异步确认的Transactional Outbox 模式实现最终一致性。
二、 现场诊断与双写一致性比对工具链
在切流过程中,如何确保新微服务写入的数据与旧单体数据库完全一致?
1. 使用 Canal 进行 Binlog 数据镜像比对
通过 Canal 实时订阅新旧数据库的 Binlog,校验关键字段:
# 检查 Canal 订阅任务状态 > 说明:文中场景、阈值和数字用于说明排查或设计方法;上线前应结合本服务版本、配置和压测结果复核。 curl -X GET http://localhost:8089/api/v1/canal/destinations/order_diff/status # 使用 MySQL 命令行对比两边数据库同一订单 ID 的 Hash 校验码 > 说明:文中场景、阈值和数字用于说明排查或设计方法;上线前应结合本服务版本、配置和压测结果复核。 mysql -h prod-db-new.cluster.internal -u readonly -p -e \ "SELECT MD5(CONCAT(id, order_sn, amount, status)) FROM order_db.t_order WHERE id = 10086;" mysql -h prod-db-old.cluster.internal -u readonly -p -e \ "SELECT MD5(CONCAT(id, order_sn, amount, status)) FROM monolith_db.t_order_legacy WHERE id = 10086;"三、 生产级 Java Transactional Outbox 代码实现
以下代码示范了如何在拆分出的新微服务中,基于 Spring Boot 3 + Spring Data JPA + Spring Event 实现生产级 Transactional Outbox 模式,确保业务数据落盘与事件消息发送的绝对一致。
package com.example.order.domain.service; import com.example.order.domain.entity.OrderEntity; import com.example.order.domain.entity.OutboxEventEntity; import com.example.order.domain.repository.OrderRepository; import com.example.order.domain.repository.OutboxEventRepository; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Map; @Slf4j @Service @RequiredArgsConstructor public class OrderDecompositionService { private final OrderRepository orderRepository; private final OutboxEventRepository outboxEventRepository; private final ObjectMapper objectMapper; /** * 创建订单:本地数据库更新与 Outbox 消息写入在一个本地事务中完成 */ @Transactional public Long createOrderInNewMicroservice(Long userId, BigDecimal amount) throws Exception { // 1. 保存新微服务领域实体 OrderEntity order = new OrderEntity(); order.setUserId(userId); order.setAmount(amount); order.setStatus("CREATED"); order.setCreatedAt(LocalDateTime.now()); OrderEntity savedOrder = orderRepository.save(order); // 2. 构建事件 Payload Map<String, Object> eventPayload = Map.of( "orderId", savedOrder.getId(), "userId", savedOrder.getUserId(), "amount", savedOrder.getAmount(), "eventType", "ORDER_CREATED_EVENT" ); // 3. 写入同库的 Outbox 消息表 (与 OrderEntity 共享同一个 MySQL 本地事务) OutboxEventEntity outboxEvent = new OutboxEventEntity(); outboxEvent.setAggregateType("ORDER"); outboxEvent.setAggregateId(savedOrder.getId().toString()); outboxEvent.setType("ORDER_CREATED"); outboxEvent.setPayload(objectMapper.writeValueAsString(eventPayload)); outboxEvent.setStatus("PENDING"); outboxEvent.setCreatedAt(LocalDateTime.now()); outboxEventRepository.save(outboxEvent); log.info("新微服务成功创建订单并写入 Outbox 事务记录,OrderId: {}", savedOrder.getId()); return savedOrder.getId(); } }package com.example.order.infrastructure.scheduler; import com.example.order.domain.entity.OutboxEventEntity; import com.example.order.domain.repository.OutboxEventRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Slf4j @Component @RequiredArgsConstructor public class OutboxEventPublisherScheduler { private final OutboxEventRepository outboxEventRepository; // 假设注入 RocketMQTemplate 或 RabbitTemplate // private final RocketMQTemplate rocketMQTemplate; /** * 轮询定时任务发送 Outbox 消息到 MQ (可替换为 Debezium CDC 零延迟监听) */ @Scheduled(fixedDelay = 1000) @Transactional public void publishPendingOutboxEvents() { List<OutboxEventEntity> pendingEvents = outboxEventRepository.findTop50ByStatusOrderByCreatedAtAsc("PENDING"); for (OutboxEventEntity event : pendingEvents) { try { // 发送消息到 MQ 支撑下游单体旧系统同步 log.info("正在投递 Outbox 事务消息到 MQ,EventId: {}", event.getId()); // rocketMQTemplate.convertAndSend("order-event-topic", event.getPayload()); // 标记为 PROCESSED 处理完成 event.setStatus("PROCESSED"); outboxEventRepository.save(event); } catch (Exception e) { log.error("投递 Outbox 事件失败,等待下一次重试,EventId: {}", event.getId(), e); } } } }四、 单体拆分演进效果对比
通过绞杀者模式对 60 万行巨石系统进行了为期 3 个月的分步拆分,系统的各项工程指标获得了质的飞跃:
| 拆分评估指标维度 | 拆分前 (单体巨石工程) | 拆分后 (微服务+Outbox 最终一致) | 改善与提升效果 |
|---|---|---|---|
| 单次全量编译打包耗时 | 18 分钟 | 45 秒 (独立微服务) | 构建效率提升 95.8% |
| 生产环境发布频次 | 两周 1 次 (需全员拉齐) | 每天 10+ 次 (独立发布) | 发布敏捷度拉满 |
| 故障隔离范围 | 1 个 Bug 拖垮整个进程 | 仅影响单个微服务 Pod | 故障半径缩小 90% |
| 分布式事务一致性达标率 | 65% (分布式下滥用 @Transactional) | 99.999% (Outbox 消息保证) | 避免数据丢失 |
| 数据库 CPU 平均利用率 | 85% ~ 98% (大表 Join 锁死) | 20% ~ 30% (库表彻底解耦) | 数据库压力大幅收敛 |
巨石系统的解耦绝不是一次完成的冲动行为。采用绞杀者模式分步切流,在入口处做好流量镜像,在存储层基于 Transactional Outbox 筑牢数据最终一致性防线,才是大型系统架构演进最稳健的路径。