单体系统怎样分步拆成服务

📅 2026/8/27 2:52:07
单体系统怎样分步拆成服务
单体系统怎样分步拆成服务 单体巨石架构的生产困境在企业级应用演进的初期单体架构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 MapString, 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() { ListOutboxEventEntity pendingEvents outboxEventRepository.findTop50ByStatusOrderByCreatedAtAsc(PENDING); for (OutboxEventEntity event : pendingEvents) { try { // 发送消息到 MQ 支撑下游单体旧系统同步 log.info(正在投递 Outbox 事务消息到 MQEventId: {}, 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 筑牢数据最终一致性防线才是大型系统架构演进最稳健的路径。