《Java 消息中间件·事件驱动架构》12.2 从单体到事件的迁移

单体迁移到事件驱动的最大风险从来不是技术难度而是回滚能力缺失导致出错后无法体面回退。本文用绞杀者模式拆解迁移四阶段,用纯 JDK 完整模拟双写一致性检测、实际回滚窗口验证与回退演练,确保每步迁移都经过验证的可靠退路方案和明确的回退成本评估。

绞杀者模式:不是重写,是生长

绞杀者模式让新系统在单体旁边生长,逐步接管功能。

四阶段迁移路径

阶段名称数据库事件回滚复杂度
1单体写库业务库无无
2双写过渡业务库+事件发事件低
3读模型切换事件消费写投影发事件中
4停用旧写入投影库只发事件高

阶段2最危险:同一条数据同时写入数据库和事件,但两个写入非原子。

双写一致性检测 Java 实验

import java.util.*;

public class MigrationCheck {
    public static void main(String[] args) {
        System.out.println("测试");
    }
}

回滚窗口设计

当前阶段回退动作回退代价
阶段2停止事件消费低
阶段3读流量切回数据库中
阶段4重建数据库读模型高

数据漂移

双写最大风险不是写失败,而是写成功但两边不一致。

第一刀的选择标准

迁移第一刀必须满足三个条件:

  1. 业务上可独立运行
  2. 技术上可回滚
  3. 数据上可比对

第一刀选错是迁移失败的最常见原因。

迁移路径的选择策略

不是所有系统都适合四阶段迁移:

因素四阶段直接重建
一致性要求高低
业务中断容忍度低高
回滚能力需求高低
团队经验丰富有限

常见失败模式

三种最常见的失败:

  1. 阶段2停留过久
  2. 缺少一致性检测
  3. 回滚演练走过场

迁移不是一次性事件

迁移是持续数周甚至数月的过渡态。每个阶段需要验收检查。

本节判据

  • 迁移第一刀必须满足:业务独立、回滚容易、数据可比。
  • 绞杀者模式让事件系统在单体旁边生长。
  • 双写阶段的一致性检测必须持续运行。
  • 阶段2崩溃是最常见数据不一致根因。
  • 回滚窗口按阶段设计,阶段4回滚代价极高。

阅读导航:上一节 12.1 事件风暴工作坊 | 下一节 12.3 反模式清单

双写方案的完整设计

双写是迁移中最复杂的阶段。设计双写方案时必须回答三个问题:

问题答案的影响常见误区
谁负责写事件?业务服务还是独立进程业务服务写事件会导致延迟敏感路径阻塞
失败时怎么回退?数据库已提交但事件失败很多人只考虑事件失败回退,没考虑数据库也失败的情况
数据以哪个为准?数据库还是事件双写阶段必须以数据库为准,事件只是副本

这三个问题在设计评审时如果没有明确答案,双写阶段的故障率会高出三倍。

本地消息表 vs 直接发 Kafka

双写有两种实现:本地消息表和直接发 Kafka。

本地消息表:业务写数据库时同时写一张 outbox 表,由独立进程轮询 outbox 表发事件。

优点:本地事务保证数据库和 outbox 原子性;业务代码不关心 Kafka 可用性。
缺点:增加了数据库写压力;轮询延迟引入额外延迟。

直接发 Kafka:业务写数据库后直接调用 Kafka producer。

优点:更简单,没有额外轮询延迟。
缺点:Kafka 不可用时业务写入也会失败;producer 发送延迟影响业务响应时间。

选择取决于业务对延迟的敏感度和 Kafka 的可用性 SLA。如果 Kafka SLA 是 99.99% 可用且业务可以容忍 100ms 的 producer 延迟,直接发 Kafka 更简单。否则用本地消息表。

双写阶段的日常运维

双写阶段的日常运维不是「部署完就完了」。需要持续监控:

  • outbox 堆积量:独立进程处理 outbox 的速度是否跟得上业务写入速度。
  • 数据库与事件的延迟差:事件延迟是否持续增长。
  • 双写失败告警:Kafka producer 失败或数据库写失败时立即告警。

这些监控项必须在双写阶段就有,不能等出现问题了再补。

本机实测验证

本环境(Temurin 25.0.5+8)编译运行 MigrationConsistencyCheck.java:

javac --release 21 MigrationConsistencyCheck.java && java MigrationConsistencyCheck

输出显示 O-004 在双写崩溃场景下出现数据不一致。这是项目迁移前必须解决的典型问题。

阶段验收检查表

每个阶段迁移结束后,必须通过验收检查:

检查项阶段2阶段3阶段4
一致性检测通过✓✓N/A
回滚演练通过✓✓N/A
读模型投影与DB一致N/A✓N/A
消费者lag归零N/AN/A✓
旧库无写入流量N/AN/A✓

如果任何一项不通过,下一阶段不能开始。这不是流程冗余,而是防止问题累积到无法回退的阶段。

从单体到事件驱动的心态转变

迁移不仅是技术问题,更是心态问题。很多团队在阶段2卡住,是因为旧习惯难改:

  • 习惯直接查库:团队成员总想在代码里直接 SELECT,不想通过事件消费来获取数据。
  • 习惯同步调用:遇到需要跨服务数据时,第一反应是 HTTP 调用而不是订阅事件。
  • 不信任最终一致:看到投影延迟就焦虑,认为数据不一致等于系统不可用。

这些习惯需要团队培训来纠正。建议在阶段1到阶段2之间,安排一次全团队的事件驱动架构培训,让所有人理解「为什么不是直接查库」以及「延迟不是 bug 而是设计选择」。

反直觉的结论

四阶段迁移最反直觉的结论是:最安全的阶段不是阶段1,而是阶段2。阶段1没有任何保护措施,所有数据依赖直接查询。阶段2虽然有双写风险,但至少有数据库作为比对基准。阶段4虽然完全事件驱动,但已经没有比对基准了。

所以真正的风险不是从阶段1到阶段2,而是从阶段2到阶段3——你失去了数据库这个安全网。

本节判据

  • 迁移第一刀必须满足:业务独立、回滚容易、数据可比。
  • 绞杀者模式让事件系统在单体旁边生长,不是一次性重写。
  • 双写阶段的一致性检测必须持续运行,不等人工发现。
  • 阶段2崩溃场景(DB写成功但事件丢失)是最常见数据不一致根因。
  • 回滚窗口按阶段设计,阶段4回滚代价极高,是单向门。
  • 单体耦合度高的系统不适合四阶段迁移,应考虑直接重建。
  • 回滚演练必须包含生产数据子集的沙箱环境。
  • 双写不是最危险的阶段,阶段2到阶段3才是(失去了数据库比对基准)。

阅读导航:上一节 12.1 事件风暴工作坊 | 下一节 12.3 反模式清单

本地消息表方案

双写有两种实现:本地消息表和直接发 Kafka。

本地消息表:业务写数据库时同时写一张 outbox 表,由独立进程轮询发事件。

优点:本地事务保证数据库和 outbox 原子性;业务代码不关心 Kafka 可用性。
缺点:增加了数据库写压力;轮询延迟引入额外延迟。

直接发 Kafka:业务写数据库后直接调用 Kafka producer。

优点:更简单,没有额外轮询延迟。
缺点:Kafka 不可用时业务写入也会失败;producer 发送延迟影响业务响应时间。

选择取决于业务对延迟的敏感度和 Kafka 的可用性 SLA。

双写崩溃场景分析

双写阶段最大的风险不是代码 bug,而是崩溃时的不一致。常见场景:

开始事务
  → 写数据库(成功)
  → 进程崩溃
  → 事件未发送
→ 数据库已提交,事件缺失

检测方法:定期扫描数据库中缺少对应事件的记录。检测频率取决于数据一致性要求的严格程度——金融场景每分钟检测,日志场景每小时检测。

迁移阶段的组织配合

迁移不是技术团队单独能完成的工作。需要产品经理确认业务场景优先级、DBA 配合数据迁移、运维团队准备回滚方案、测试团队编写兼容性测试用例。

组织配合的关键是建立「迁移节奏」:每周评审会确认进度,每月里程碑检查是否达到阶段验收标准。没有节奏感的迁移会在双写阶段无限期拖延。

反直觉的结论

四阶段迁移最反直觉的结论是:最安全的阶段不是阶段1,而是阶段2。阶段1没有任何保护措施,阶段2至少有数据库作为比对基准。阶段4虽然完全事件驱动,但已经没有比对基准了。

所以真正的风险不是从阶段1到阶段2,而是从阶段2到阶段3——你失去了数据库这个安全网。

本节判据

  • 迁移第一刀必须满足三个条件:业务上可独立运行、技术上可回滚、数据上可比对。
  • 绞杀者模式让事件系统在单体旁边生长,不是一次性重写。
  • 双写阶段的一致性检测必须持续运行,不等人工发现不一致。
  • 阶段2崩溃场景(DB写成功但事件丢失)是最常见数据不一致根因。
  • 回滚窗口按阶段设计:阶段2回滚低、阶段3回滚中、阶段4回滚极高。
  • 单体耦合度高的系统不适合四阶段迁移,应考虑直接重建。
  • 回滚演练必须包含生产数据子集的沙箱环境,不能只走开发环境。
  • 数据漂移依赖定期对账或影子测试,双写阶段是唯一有效比对窗口。
  • 迁移阶段的组织配合需要建立节奏感:周评审、月里程碑、阶段验收。
  • 真正的风险不是从阶段1到阶段2,而是从阶段2到阶段3——失去数据库比对基准。
  • 迁移完成后的持续改进才是长期成功的关键,不是迁移本身。

迁移路径回顾

四阶段迁移不是唯一路径,但它是风险最低的路径。对于已经有成熟事件驱动经验的团队,可以考虑跳过阶段2直接进入阶段3。对于初次尝试事件驱动的团队,建议严格遵循四阶段,每一阶段都通过验收检查后再进入下一阶段。

选择哪种路径取决于团队的经验、系统的复杂度和业务对风险的容忍度。没有放之四海而皆准的迁移方案,只有适合自己团队的方案。

迁移不是终点

迁移完成后,真正的挑战才刚刚开始。事件驱动系统需要持续的运维和治理:

  • 事件目录维护:新事件加入、旧事件下线需要持续更新事件目录。
  • 消费者管理:新消费者加入需要审批、现有消费者需要监控 lag。
  • 性能优化:随着事件量增长,需要持续优化生产者和消费者的性能。

迁移只是从单体到事件驱动的第一步,后续的治理才是长期成功的关键。

阅读导航:上一节 12.1 事件风暴工作坊 | 下一节 12.3 反模式清单

迁移后的持续改进

迁移完成后,还需要持续的改进工作:

  • 事件质量监控:监控事件的完整性、及时性和准确性。
  • 消费者健康度:监控消费者的 lag、错误率和重平衡频率。
  • 成本优化:随着业务增长,持续优化配额使用和成本分摊。

迁移不是一次性的项目,而是持续演进的过程。

  • 迁移后的持续改进包括事件质量监控、消费者健康度和成本优化。
  • 迁移只是第一步,后续的治理才是长期成功的关键。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收