← 返回博客

从单体到微服务迁移实战:6种策略与避坑指南

微服务拆分讲的是"怎么拆",迁移讲的是"怎么把拆出来的服务安全上线"。本文给出6种经过验证的迁移策略——绞杀者模式、事件拦截、数据库优先拆分、并行运行、特性开关、Bulk Sync。适合正在做迁移方案的架构师和技术负责人。含迁移路线图和时间预估。【点击查看完整策略对比表】

先说结论:拆分和迁移是两回事

很多团队把”拆出来”当成了”迁完”——代码从单体里抽出来、新建一个项目、部署上去,就觉得完事了。但真正的迁移是从旧系统安全下线的那一刻才结束的。

拆分的核心问题是**“边界在哪里”,迁移的核心问题是”怎么在不中断业务的前提下把流量切过去”**。

本文给出 6 种经过真实项目验证的迁移策略,按风险从低到高排列,每种附带适用条件和具体步骤。


策略一:绞杀者模式(Strangler Fig)

风险等级:★★☆☆☆ | 推荐指数:★★★★☆

原理

在单体前端(API 网关或反向代理层)做流量拦截:新功能直接走新服务,旧功能继续走单体。随着时间推移,单体被”绞杀”至消失。

客户端 → 网关 / 反向代理
         ├── 新功能 → 新微服务
         └── 旧功能 → 单体

适用条件

  • 单体有统一的 API 网关或可以接入反向代理(Nginx / Envoy / APISIX)
  • 新功能和旧功能有清晰的 URL 路径或域名区分
  • 业务迭代节奏允许新旧代码并行维护一段时间

具体步骤

  1. 在网关层配置路由规则,新增的端点指向新服务
  2. 旧单体中被替换的端点不再接收新流量,但保留旧代码作为回滚
  3. 当单体内所有端点都被替换后,下线单体
  4. 移除网关中的旧路由规则

常见陷阱

  • 网关路由爆炸:每拆一个服务就加一条路由,N 个服务后路由配置变成一团乱麻。解决方案:用服务发现(Consul / Nacos)替代硬编码路由。
  • 绞杀不彻底:单体内常有一些”谁都调用”的工具函数(日期处理、ID 生成、校验逻辑),拆完服务后这些函数依然残留在单体里,导致单体无法下线。迁移前先识别并提取公共库。

策略二:事件拦截(Event Interception)

风险等级:★★★☆☆ | 推荐指数:★★★☆☆

原理

如果单体系统内部已经用了消息队列或事件总线,可以在事件层面做拦截:新服务监听原本流向单体的事件,处理完后发布结果事件,单体消费结果后更新状态。

事件源 → 消息队列 / 事件总线
         ├── 旧监听器 → 单体(逐步停用)
         └── 新监听器 → 微服务(逐步接管)

适用条件

  • 单体已使用消息队列(RabbitMQ / Kafka / Redis Streams)
  • 核心业务流程是事件驱动的(订单状态流转、工单处理)
  • 可以接受最终一致性

具体步骤

  1. 新服务订阅原有事件流,注册为新的消费者
  2. 新服务处理事件后,发布”已处理”事件
  3. 单体的旧消费者逐渐降级——先停止写操作,只做读操作
  4. 验证新服务的数据正确性后,移除单体的旧消费者

常见陷阱

  • 重复消费:新旧消费者同时运行期间,同一个事件可能被处理两次。需要业务层面的幂等设计或去重机制。
  • 事件顺序:如果单体依赖事件的时序(先创建后更新),新服务必须保证消费顺序和原系统一致——同一个实体 ID 的事件走同一个分区。

策略三:数据库优先拆分(Database-First Split)

风险等级:★★★★☆ | 推荐指数:★★★☆☆

原理

先动数据库,再动代码。把单体数据库中的表按业务域做逻辑隔离(schema 拆分或实例拆分),API 层依然走原来的代码,但数据源指向新的数据库实例。等数据稳定后,再将 API 层逐步迁移到新服务。

适用条件

  • 单体数据库是迁移的最大瓶颈(性能不够、单表过大、查询耦合)
  • 数据域边界相对清晰(按模块或业务线可以区分表归属)
  • 有数据库中间件或代理层可以做读写分离

具体步骤

  1. 识别业务域对应的表集合
  2. 创建新的数据库实例,将目标表连同 3-7 天的历史数据同步过去
  3. 修改单体中的数据访问层,将目标表的读写指向新数据库
  4. 运行双写 + 对账(见下文),确保数据一致
  5. 稳定运行 1-2 周后,从单体中移除这些表的 DDL 和 DML
  6. 开始将这些表的业务逻辑拆成独立服务

常见陷阱

  • 跨域 JOIN:原先单体里一条 SQL 就能 JOIN 两个域的表,拆库后必须改成 API 调用或数据冗余。拆分前先扫描所有 JOIN 查询,识别哪些是真需要跨域的。
  • 分布式事务:拆库后,原本单库里的事务写多个表变成了跨库分布式事务。尽量用最终一致性替代,非用不可的场景走 Saga。

策略四:并行运行(Side-by-Side)

风险等级:★★★★☆ | 推荐指数:★★☆☆☆

原理

新服务与单体同时运行,处理同样的请求,但只有单体的结果对外输出。通过对比两个系统的输出差异来验证新服务的正确性。

客户端 → 网关
         ├── 单体(主 → 返回给客户端)
         └── 新服务(影子 → 记录结果,不返回)

适用条件

  • 对正确性要求极高(金融、交易、账务)
  • 新服务的逻辑可以完全独立于单体运行(不修改单体状态)
  • 有足够的计算资源运行两个完整的系统

具体步骤

  1. 新服务部署为”影子模式”——接收真实流量但不影响业务
  2. 每次请求同时在单体和新服务中执行
  3. 对比两个系统的输出结果(响应体、数据库变更)
  4. 差异分析并修复
  5. 当差异率低于 0.01% 且所有已知差异都已修复后,切换主服务

常见陷阱

  • 副作用:新服务虽然是影子模式,但如果它有写数据库的操作,就会产生”幽灵数据”。影子服务必须严格只读,或用隔离数据库实例。
  • 性能开销:每个请求跑两遍,系统资源消耗翻倍。评估流量峰值下的资源容量,必要时只对部分流量做影子对比。

策略五:特性开关切换(Feature-Flag Cutover)

风险等级:★★☆☆☆ | 推荐指数:★★★★☆

原理

在代码中埋入特性开关(Feature Flag),通过配置中心控制某个功能走单体还是新服务。开关可以按用户、按租户、按百分比灰度。

if (featureFlag('order-service')) {
  return await newOrderService.process(order);
} else {
  return await legacyMonolith.process(order);
}

适用条件

  • 代码中已有或可以引入特性开关系统(LaunchDarkly / 自建配置中心)
  • 迁移的功能可以按用户或请求维度独立路由
  • 需要快速回滚能力

具体步骤

  1. 在单体中为待迁移的功能增加特性开关
  2. 新服务部署上线
  3. 将开关打开到 1% 的流量(内部测试用户)
  4. 逐步扩大到 10%、50%、100%
  5. 100% 稳定运行 1 周后,移除旧代码和特性开关

常见陷阱

  • 开关泄露:特性开关的 key 散落在代码各处,迁移完成后可能有好几个忘记删的开关。建议:所有开关集中定义在一个配置文件 / API 中,迁移完成后统一清理。
  • 用户感知差异:同一个用户在不同请求中可能被路由到不同的系统(有的走新服务、有的走单体),导致状态不一致。确保用户级别的路由一致性——同一个用户的请求始终走同一个系统。

策略六:Bulk Sync + 数据对账

风险等级:★★★☆☆ | 推荐指数:★★★★☆

这不是独立的迁移策略,而是所有迁移策略都需要的基础设施——数据同步和对账机制。

双写模式

单体写入 → DB_old
        ↘ DB_new(通过 CDC / 应用层双写同步)
  • 应用层双写:单体每次写入时,同步调用新服务的 API 或写入新库。侵入性强,但实时性最好。
  • CDC 同步:通过 Debezium / Canal 监听单体数据库的 binlog,实时同步到新库。无侵入,但依赖 binlog 格式和 Schema 兼容性。

对账任务

每小时(或每日)跑一次,对比新旧库:

对账项对比内容告警阈值
记录数COUNT(*) 差异> 0.1%
金额合计SUM(amount) 差异> 0.01%
最新时间戳MAX(updated_at) 差异> 5 分钟
抽样数据随机 100 条逐字段对比任意字段不一致

对账差异需要自动告警(飞书/钉钉/邮件),暂停切量操作,直到差异定位并修复。


迁移路线图:从零到全量切换

阶段目标时长关键动作
第 0 阶段:准备可观测性就绪1-2 周部署 APM(链路追踪)、集中日志、业务监控大盘
第 1 阶段:试点拆一个简单服务2-4 周选一个无状态、低耦合的模块(如通知服务),用绞杀者模式迁移
第 2 阶段:数据拆分数据库逻辑隔离3-6 周按业务域拆分 schema,搭建 CDC 同步 + 对账
第 3 阶段:核心迁移拆 2-3 个核心服务4-8 周用特性开关灰度切换,每个服务独立验证
第 4 阶段:收尾单体下线2-4 周清理单体内残存代码,移除网关旧路由,归档单体项目

总时长预估:中等复杂度系统(~50 张表、5-8 个业务域)的全量迁移约 3-6 个月。


总结

策略风险适用场景优势
绞杀者模式有 API 网关的系统风险可控,渐近迁移
事件拦截已用消息队列的系统天然解耦
数据库优先数据库是瓶颈的系统从最痛点入手
并行运行金融级正确性要求100% 验证
特性开关需要灰度回滚精细控制流量

迁移成功的标志不是”新服务上线了”,而是”旧系统可以安全关机了”。

相关阅读

正在规划从单体到微服务的迁移?联系我们,说清你的系统现状与痛点,24 小时内出可行性评估。

常见问题

迁移微服务,应该先拆代码还是先拆数据库?

推荐"数据库先行"策略。先做数据库的逻辑隔离(schema 拆分、读写分离),让数据边界清晰之后,代码层面的拆分就只是 API 层的取舍问题。反过来如果先拆代码再拆数据库,你会陷入"服务拆了但还在读写同一张表"的尴尬局面,这时候再拆数据库的改动会波及已经拆出去的服务,做一次回滚级的重构。

绞杀者模式适合什么场景?

绞杀者模式(Strangler Fig)最适合有完整 API 网关或反向代理的单体系统。你在网关层做流量转发,把新功能或特定模块的请求导向新服务,旧代码继续走单体。它的最大优势是风险可控——每次只迁移一个端点,出问题只影响那一条路由。缺点是迁移周期长,单体代码和微服务代码要并行维护很久。适合业务稳定、迭代节奏慢的系统。

迁移过程中流量怎么切?

推荐灰度切换而非一刀切。用网关按用户 ID 或 tenant ID 做流量染色:先切 1% 的内部用户,观察 3-5 天确认无异常;扩展到 10% 的真实用户,持续观察;再到 50%、100%。每个阶段准备回滚方案。关键指标:P50/P99 延迟不高于迁移前 10%,错误率不高于迁移前基线,业务核心功能可通过自动化回归。

数据一致性怎么保证?

迁移期间最棘手的问题。核心策略是"双写"——新服务写入自己的数据库,同时通过事件总线同步回单体数据库(或反过来)。双写期间做数据校验:每小时跑一次对账任务,对比新旧库的关键记录数、金额合计(如果是财务数据)、最新更新时间。差异超过阈值就告警暂停切量。等双写稳定运行 1-2 周后再下线旧代码。

先拆最复杂的模块还是最简单的模块?

从最简单的模块开始。目标不是"快速见到效果",而是"建立迁移流程和信心"。最简单的模块(如字典服务、通知服务)改动面小、独立性强,拆完能快速验证整个 CI/CD、监控、部署流程是否跑通。等团队熟悉了迁移节奏,再去拆核心业务模块。简单模块的迁移周期一般是 1-2 周,核心模块可能需要 1-2 个月。

本文来自 AI Enable Harness 一线交付实践。需要同类系统或优化服务?

订阅博客更新

新文章发布后第一时间邮件通知。不定期发送,不推销。

订阅 →