MySQL与MongoDB跨库事务难题:分布式事务方案与补偿机制实践

发布时间:2026/9/16 23:59:00
MySQL与MongoDB跨库事务难题:分布式事务方案与补偿机制实践 1. 问题背景接口里同时操作MySQL和MongoDB为什么事务成了老大难先还原一个典型场景。你在写一个订单创建接口业务逻辑是这样的先往MySQL的orders表插入一条订单记录然后再往MongoDB的order_logs集合写入一条流水日志。单看任何一步都没问题MySQL这边用Spring的Transactional一包出错了自动回滚。MongoDB那边单独操作也没毛病。但问题出在两个数据库同时参与一个接口调用时——如果MySQL插入成功了MongoDB写入却抛了异常这时候该怎么办我最早遇到这个问题是在做一个电商中台项目订单主数据在MySQL商品快照、埋点日志、用户操作轨迹放在MongoDB一个下单接口要同时写两边的数据。上线前测试环境一切正常上线后高峰期出现了几十条订单有MySQL记录、MongoDB没日志的脏数据。排查的时候特别痛苦因为单看MySQL是完整的单看MongoDB也是完整的只有把两边数据放在一起比对才能发现问题。这也是跨数据库事务最恶心的点问题不会立刻暴露而是像欠了债一样累积等你想起来对账的时候已经是一笔烂账了。这个问题的本质是什么一句话概括MySQL事务和MongoDB事务各自只能保证自己节点内的ACID跨节点的原子性在应用层没有天然机制兜底。传统关系型数据库有XA两阶段提交协议但一是实现复杂、性能损耗大二是MongoDB虽然从4.0版本开始支持多文档事务但它的事务模型和MySQL的XA协议完全是两码事两者之间没有一个标准的全局事务协调器。所以做接口开发的人必须建立这样一个认知只要一个接口里涉及多个数据源你就等于进入了分布式事务的领域。哪怕你的接口只有一个方法、几个SQL和几条MongoDB操作它也是一个微缩版的分布式事务问题。这篇文章就围绕这个问题从原理到实操把我在实际项目中踩过的坑和沉淀下来的方案完整过一遍。2. 先搞清楚两个数据库在事务能力上的本质差异2.1 MySQL InnoDB事务成熟、完备、依赖回滚日志MySQL默认使用InnoDB存储引擎事务机制经过二十多年迭代已经非常成熟。它的核心是redo log和undo log的配合redo log保证已提交事务的持久性undo log负责记录事务执行前的数据镜像一旦事务需要回滚就通过undo log把数据恢复到操作之前的状态。写一个例子。你在一个事务里执行了三条SQLSTART TRANSACTION; INSERT INTO orders (id, user_id, amount) VALUES (1, 1001, 99.00); UPDATE user_balance SET balance balance - 99.00 WHERE user_id 1001; INSERT INTO order_logs (order_id, action) VALUES (1, CREATE); COMMIT;如果第三条SQL因为某个字段超长报错了InnoDB会自动把前两条SQL的影响也撤销掉。这个能力是数据库内核提供的应用层不需要写任何回滚代码。2.2 MongoDB事务4.0之后才有约束条件多MongoDB从4.0版本才开始支持多文档事务而且只支持副本集模式4.2版本才扩展到分片集群。但它的使用限制非常多事务默认超时时间为60秒超过时间事务自动中止。事务期间不能修改集合结构不能建索引、不能删除集合。写入操作要求事务内的所有文档必须存在于同一个分片如果使用分片集群。事务内的读操作默认读关注为primary不支持从节点读取。更重要的是MongoDB的事务是基于会话session的。你在代码里开启一个事务时必须显式传入session参数所有操作都得绑定这个session。这和MySQL那种隐式事务的感觉完全不同。// MongoDB事务的标准写法Java驱动 ClientSession session mongoClient.startSession(); try { session.startTransaction(); collection.insertOne(session, doc1); collection.insertOne(session, doc2); session.commitTransaction(); } catch (Exception e) { session.abortTransaction(); } finally { session.close(); }注意到没有MongoDB的事务代码比MySQL繁琐得多。而且如果你不小心漏传了session操作不会报错而是直接以非事务方式执行了——这个坑非常隐蔽。2.3 为什么两个数据库无法自动联动回滚MySQL没有能力感知MongoDB的事务状态MongoDB也不知道MySQL那边发生了什么。两者之间没有共享的事务协调器。所以当你在一个接口里先写MySQL再写MongoDB然后MongoDB写入失败MySQL那边已经提交的事务是不可能自动回滚的。这里需要区分一个概念你如果在Spring的Transactional方法里既操作了MySQL的Mapper又操作了MongoTemplateSpring会不会自动帮你把两个都回滚答案是Spring只能回滚实现了Spring事务管理接口的数据源对于MongoDBSpring Data MongoDB确实也提供了一部分事务支持但前提是它要接管MongoDB的事务生命周期并且你要使用MongoDB的session开启事务。Spring不会因为你MySQL抛了异常就自动去调用MongoDB的abortTransaction。反过来也一样。打个比方你请了两个施工队分别负责水电和木工结果木工做坏了水电工已经收工走人。你不可能指望水电工自己回头把已经铺好的管线拆了重新来——除非你有办法通知到他。这里的通知机制就是分布式事务中所谓的协调器而Spring默认并没有这个东西。3. 接口层处理跨库事务的六种常见方案与选型权衡既然数据库层面无法自动联动那就只能在应用层想办法。这六种方案我都在真实项目中验证过或调研过各有适用场景没有银弹。3.1 两阶段提交XA理论上最正统实践中几乎不用XA协议是X/Open组织提出的分布式事务规范核心思想是通过事务管理器协调多个资源管理器完成两阶段提交第一阶段所有参与者各自执行事务并锁定资源prepare第二阶段事务管理器统一决定提交或回滚commit/rollback。MySQL InnoDB支持XA事务Spring通过JTAJava Transaction API可以管理多个XA数据源。但MongoDB不支持标准的XA协议所以这个方案在MySQLMongoDB的组合下基本无法落地。即便同样是两个MySQL数据库我也建议慎用XA因为两阶段提交的锁定时间很长在高并发接口下会产生大量的锁等待和超时性能损耗非常可观。3.2 本地消息表经典可靠方案适合中等并发这个方案的核心思路是把保证多数据源一致这件事转化为保证本地数据库事务异步消息投递。具体做法在MySQL中创建一个消息表message_outbox和业务表放在同一个MySQL实例里。在同一个本地事务中写入业务数据和消息记录。后台有一个定时任务扫描message_outbox里状态为待发送的记录把消息发送到MQ比如RocketMQ、RabbitMQ。消费者收到消息后执行MongoDB的写入操作。写成功后回调接口修改消息状态为已发送写失败则重试。这个方案的好处是MySQL那边的事务是真正的本地事务可靠度极高。MongoDB的写入变成了一种异步补偿动作即使失败也可以通过重试机制最终达成一致。但缺点也很明显接口的实时性变差了。原本一条接口请求直接写两个库现在变成先写MySQL再通过MQ异步写MongoDB数据最终一致的时间取决于消息投递和消费的速度。3.3 事务消息消息队列支持的事务方案RocketMQ的事务消息本质上是本地消息表方案的中间件化。它把消息表的管理移植到了MQ服务端但应用层仍需实现半消息half message的回查逻辑。这个方案比手动建表要省心但增加了对MQ中间件版本的依赖。如果你的项目已经有RocketMQ我推荐优先考虑这种方式。3.4 SAGA事务模式拆解为多个子事务逐层补偿SAGA模式来源于分布式事务的学术研究核心思想是把一个全局事务拆分为多个子事务每个子事务有对应的补偿操作。比如一个下单接口拆成创建订单MySQL→ 扣减库存MySQL→ 写操作日志MongoDB。任何一个子事务失败就反向执行前面所有成功子事务的补偿操作。这种模式非常契合MySQLMongoDB的场景因为它的本质就是每个参与方管理自己的事务应用层通过补偿逻辑来兜底。但SAGA要求你对每个子事务都设计出对应的补偿逻辑比如创建订单的补偿是删除订单扣减库存的补偿是加回库存。补偿逻辑写起来很繁琐而且补偿操作本身也需要具备幂等性。3.5 最终一致性先写主库再异步同步如果MongoDB里面存的不是核心业务数据比如日志、流水、快照那可以考虑把它降级为异步同步。核心思路是接口只保证MySQL的强一致MongoDB的数据通过后续的定时任务或MQ进行同步。这可能听起来像是偷懒但结合真实业务来说很多场景根本不需要MongoDB和MySQL保持强一致。比如订单日志用户下单后立刻查不到日志或者日志延迟几秒才出现对用户无感知对业务也基本无影响。这种方案代码量最少、性能影响最小是我在实际项目中用得最多的。3.6 保持一致性的兜底方案对账与修复任务无论你选了哪种方案我都建议在系统层面加一道对账机制。定期比如每5分钟跑一个比对任务找出MySQL有记录但MongoDB没记录的数据自动或人工触发生成缺失的MongoDB文档。这个方案不是解决事务问题的而是解决万一前面的方案也有漏洞的问题。算是最后一道保险。3.7 方案选型对比表方案实时性一致性代码量维护成本适用场景XA两阶段提交强同步强一致中高不推荐用于MySQLMongoDB本地消息表异步最终一致高中中高并发、需可靠投递事务消息异步最终一致中中已使用RocketMQ的项目SAGA补偿半同步最终一致非常高高业务流程复杂、有现成补偿逻辑最终一致性同步异步最终一致低低非核心数据、日志类数据对账兜底异步最终一致中中所有方案都应加一道4. 实操方案接口中用一个可靠且改动可控的回滚机制现在具体到代码层面。考虑到大多数团队的技术栈是Spring Boot MyBatis Spring Data MongoDB我给你一套已经在生产环境跑过很久的方案本地事务 Mongo手动提交/回滚 补偿兜底。这个方案不需要额外引入MQ也不用写太复杂的SAGA编排适合业务接口数量在几十个到上百个的中型项目。4.1 整体设计思路核心策略是**MongoDB后写、失败补偿**先在自己的MySQL事务里完成所有MySQL侧的写入并立刻提交。然后执行MongoDB的写入操作通过session开启事务写成功后提交。如果MongoDB写入失败则触发MySQL侧的补偿操作把刚才在MySQL中写入的数据删除或标记为失效。这样MySQL是正向事务MongoDB是尽力而为如果MongoDB失败补偿逻辑来修正。相比先写MongoDB再写MySQL的方案这个顺序有一个好处MySQL是主要数据源可靠性更高先提交MySQL可以降低主数据丢失的风险。4.2 代码骨架示例Service Slf4j public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private MongoTemplate mongoTemplate; Autowired private MongoTransactionManager mongoTransactionManager; Override Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateRequest request) { // Step 1: 生成主订单数据 OrderDO order new OrderDO(); order.setOrderId(request.getOrderId()); order.setUserId(request.getUserId()); order.setAmount(request.getAmount()); order.setStatus(CREATED); orderMapper.insert(order); // Step 2: 写MongoDB流水先不入集合等MySQL提交后再写 // 这里不做任何操作 } }上面这段代码只是最基础的MySQL本地事务。下面要做的就是改造把MongoDB的写入挪到MySQL事务提交之后。4.3 使用TransactionSynchronizationManager在事务提交后执行MongoDB操作Spring提供了TransactionSynchronizationManager可以注册一个同步回调在当前事务提交后执行后续动作。这是一个非常实用的机制很多资深开发也未必用过。Service Slf4j public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private MongoTemplate mongoTemplate; Override Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateRequest request) { OrderDO order buildOrderDO(request); orderMapper.insert(order); // 注册事务同步回调在MySQL事务提交后执行MongoDB写入 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { try { // 这个回调里执行MongoDB写入 OrderLogDO logDO buildOrderLogDO(order); mongoTemplate.insert(logDO, order_logs); } catch (Exception e) { log.error(写入MongoDB失败, orderId{}, request.getOrderId(), e); // 这里需要触发补偿机制将MySQL中的记录标记为失效 orderMapper.markFailed(request.getOrderId()); } } }); } }这段代码的关键在于afterCommit()方法里的逻辑不受MySQL本地事务的控制MySQL事务已经提交了所以这里的异常不会触发MySQL回滚。补偿动作就得自己写。4.4 手动控制MongoDB事务如果MongoDB也是核心数据源如果MongoDB里的数据也很核心不能简单靠重试来兜底那就需要显式开启MongoDB的事务。这里推荐使用MongoTransactionManager它可以和Spring的事务抽象集成但要留意它的配置方式。Configuration public class MongoConfig { Bean public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { return new MongoTransactionManager(dbFactory); } }然后在Service里使用Transactional注解但是是MongoDB的事务管理器注意要和MySQL的事务管理器区分开。实际开发里我不建议在同一个方法上同时标两个Transactional容易引起混乱。更稳妥的做法是拆分两个方法一个管MySQL一个管MongoDB在调用层做编排。Service public class OrderCreateService { Autowired private OrderTransactionService orderTransactionService; Autowired private MongoLogService mongoLogService; public void createOrder(OrderCreateRequest request) { // 1. MySQL本地事务 orderTransactionService.createOrderInMySql(request); // 2. MongoDB事务失败时调用补偿 try { mongoLogService.writeLog(request.getOrderId(), CREATE); } catch (Exception e) { // 补偿更新MySQL订单状态为LOG_WRITE_FAILED orderTransactionService.markLogWriteFailed(request.getOrderId()); throw new BizException(订单创建成功但日志写入失败); } } }4.5 补偿操作必须支持的幂等性设计补偿操作很容易踩的坑是重复执行。比如MongoDB写入实际成功了但网络超时导致接口报错补偿逻辑又执行了一次。这时候如果补偿逻辑是删除MongoDB日志就可能把已经写好的日志删掉造成数据缺失。所以所有补偿操作都必须支持幂等。简单来说不管你执行多少次结果都一样。设计方式有两种状态机方案在MySQL的orders表加一个status字段订单状态流转是单向的CREATED → LOG_WRITTEN → COMPLETED。补偿操作只有在statusCREATED时才能执行执行后变为LOG_WRITTEN_FAILED。这样即使补偿被触发两次第二次发现状态不匹配直接跳过。唯一索引方案在MongoDB的order_logs集合里给orderId加唯一索引。补偿操作先尝试查询如果已存在则直接返回成功。两种方案我都用过更推荐状态机方案因为它在MySQL层面就把补偿的边界限制住了逻辑清晰排查问题也方便。5. 常见问题与排查技巧实录5.1 问题现象MySQL有数据MongoDB没有数据这是最常见的现象。出现原因一般有三种MongoDB写入抛出了异常但被吞掉了、补偿逻辑执行失败没重试、MongoDB写入超时但实际已经成功假失败。排查思路是先看应用日志里有没有MongoDB相关的报错记录。如果没有再看补偿逻辑是否正确执行。如果补偿逻辑也执行了那就要怀疑是不是假失败——MongoDB写入成功但响应超时。针对假失败最简单的排查方式是去MongoDB里直接查一下orderId对应的数据是否存在。如果存在就说明是假失败补偿逻辑要设计成幂等的这样不会造成重复数据。5.2 问题现象MongoDB有数据MySQL没有数据这种情况多半是代码执行顺序反了先写了MongoDB再写MySQL。MySQL事务回滚后MongoDB的数据就成了孤儿数据。所以开发规范上一定要明确MongoDB必须是后写的一方。如果代码已经上线且不方便调整执行顺序可以通过定时任务扫描MongoDB里最近10分钟写入的日志数据反查MySQL如果没有关联订单则补发告警或自动删除。5.3 问题现象MongoDB事务开启时报错使用MongoDB事务时最常见的报错是Transaction numbers are only allowed on a replica set member or mongos。这个报错的意思是你的MongoDB运行在Standalone模式不支持事务。解决方法是把MongoDB改成副本集模式哪怕是单节点副本集也行。具体操作是在MongoDB的配置文件中添加replication配置replication: replSetName: rs0然后重启服务执行一次初始化rs.initiate()很多人在本地开发环境遇到这个问题多半是因为图省事直接用的是Standalone模式。改用副本集模式后事务功能就可以使用了。5.4 问题现象Spring事务和多数据源配置冲突项目里同时配置了MySQL数据源和MongoDBSpring的Transactional注解默认绑定的是DataSourceTransactionManager。如果你没有指定事务管理器Spring会按照类型自动装配但一旦有两个事务管理器比如MySQL的和MongoDB的必须明确指定。Transactional(transactionManager mysqlTransactionManager, rollbackFor Exception.class)不要图省事省略transactionManager参数否则运行期可能出现事务不生效或者事务管理器错乱的诡异问题。5.5 幂等性设计不彻底导致重复补偿接口层需要保证幂等这是一个老生常谈但经常被忽略的点。尤其涉及跨库操作时一个请求由于网络原因被前端重试两次如果接口没有幂等控制MySQL和MongoDB各写了两份数据。我的做法是在接口入口处增加一个幂等判断根据业务唯一键比如orderId在MySQL中查一下订单是否已存在存在则直接返回已有结果。这个方法虽然简单但能挡掉大部分重复请求。5.6 监控与告警如何快速发现跨库数据不一致人工比对数据不现实最好做成自动化监控。我在项目中实现过一个简易的对账任务每10分钟跑一次从MySQL的orders表查出最近10分钟内状态为CREATED或COMPLETED的订单。根据订单ID集合查MongoDB的order_logs比对是否存在。如果订单存在但日志不存在发告警通知。这段对账逻辑不复杂但价值很高。它可以发现那些补偿逻辑连自己都没处理好的情况是分布式事务体系的最后一道保险。6. 一些值得注意的底层原理解读与性能影响6.1 Transactional为什么不能跨数据源回滚很多初学者会有一个误解在一个方法上加了Transactional是不是所有数据库操作都能回滚不是。Transactional注解背后依靠的是Spring的事务管理器每个数据源需要独立的事务管理器事务边界是随着事务管理器走的。当你调用一个Mapper方法时事务是与当前的DataSource绑定的。你调用MongoTemplate时MongoDB的操作走的是MongoDB的事务管理器。两个事务管理器各管各的没有统一的提交或回滚入口。这也是我在前面强调必须自己写补偿逻辑的根本原因。6.2 MongoDB事务的性能特征与使用限制MongoDB事务引入了一个隐式的快照隔离机制在事务执行期间所有读操作只能读到一个快照的数据写操作之间会有锁冲突。如果你的事务写入了大量文档性能会显著下降。我做过一次压测在MongoDB 5.0副本集模式下单文档写入的非事务操作TPS大约12000开启事务后TPS降到2800左右。相当于打了2折到3折。所以不要轻易把高频写入操作放到MongoDB事务里能用单文档原子操作解决的尽量用单文档操作。比如更新一个嵌套字段可以用$set给某个数组追加一个元素可以用$push这些都能在单文档层面保证原子性完全不需要开启跨文档事务。6.3 连接池资源占用与事务超时MongoDB事务在执行期间会占用一个连接资源。如果一个接口开启了事务但处理速度很慢连接池会被迅速耗尽引起雪崩。解决方法是给MongoDB事务设置合理的超时时间比如5秒。在Spring Data MongoDB中可以通过配置spring.data.mongodb.transaction-timeout上限来控制也可以在代码中显式设置Session的事务选项。Java驱动的写法TransactionOptions txnOptions TransactionOptions.builder() .maxCommitTime(Duration.ofSeconds(5)) .build(); session.startTransaction(txnOptions);同理MySQL这边也要设置合理的Transactional超时时间防止长事务攒一堆未提交数据。我一般会把接口级别的超时时间设置在10秒以内。7. 更进一步什么时候要重构接口设计前面讲的都是在接口必须同时操作两个数据库的前提下做文章。但有些时候更好的解法是从接口设计层面规避掉跨库事务。7.1 将强一致操作聚合到单一数据库如果业务允许尽量把强一致的数据都放在MySQL里MongoDB只存储非核心数据。比如订单业务中orders表可以冗余一个字段存储JSON格式的商品快照而不是把商品快照单独放到MongoDB。虽然这违反了第三范式却换来了接口的简单性和强一致性。7.2 用事件驱动替代同步写入如果你是用MongoDB存日志和流水完全可以改成发送一个领域事件到MQ由MQ的消费者异步写入。这样接口本身不需要感知MongoDB的存在事务问题直接消失。7.3 评估一下你的MongoDB到底是不是刚需有些团队在项目中引入MongoDB其实只用了它存储JSON文档、日志、统计聚合数据。但在只有这类需求时MySQL的JSON字段类型8.0以上版本或者PostgreSQL的JSONB也能胜任。与其背上跨库事务的包袱还不如从一开始就统一数据源。我在一个项目中就做过这样的迁移把MongoDB中的用户行为日志迁到了MySQL用一张log表加一个script字段存JSON虽然单表数据量增长了一些但查询能力并没有下降反而因为不用跨库整个系统的稳定性和可维护性都有了明显提升。8. 针对MySQL和MongoDB安装部署与运维层面的补充文章最后补充一块很多人会遇到的问题因为它和事务回滚在运维层面的表现息息相关如果你的MongoDB安装部署不对很多事务配置根本不会生效。8.1 MongoDB必须使用副本集模式前面提到过MongoDB事务必须运行在副本集或分片集群上。很多新手按照教程安装完MongoDB默认跑的是Standalone模式结果在代码里一开启事务就报错。把MongoDB切换到单节点副本集的方法前面已经给过这里不再重复。8.2 MySQL的隔离级别对回滚行为的影响MySQL默认的隔离级别是Repeatable Read可重复读。在这个级别下事务内的所有查询都基于同一个快照所以如果事务中途有其他请求修改了数据你是感知不到的。这会导致一个问题补偿逻辑里查询状态时可能读到旧数据。举个例子订单事务内你给订单状态改成了CREATED然后执行MongoDB写入失败事务回滚。此时如果另一个线程修改了订单状态为COMPLETED补偿逻辑再去查订单时可能因为隔离级别的限制查到的还是旧状态。解决这个问题的方法是补偿逻辑使用独立的新事务去查询比如使用REQUIRES_NEW传播级别确保读到最新的已提交数据。8.3 MySQL事务开启时需要注意的DDL问题MySQL在事务中执行DDL语句比如ALTER TABLE、CREATE INDEX会造成隐式提交。如果你在一段事务逻辑里先插入业务数据再执行了一条DDL语句事务会被自动切割前面的写操作已经提交了后面的写操作如果报错是无法回滚到最开始的。这个问题在老项目中非常普遍排查起来也很隐蔽。建议开发规范明确事务方法内禁止执行业务DDL操作。8.4 数据库连接参数的正确设置MySQL的JDBC连接串中autoReconnect参数要设置为truemaxReconnectAttempts要合理配置。否则数据库连接断开时在事务内执行的第一个SQL可能直接抛异常而Spring可能还会尝试重试导致同一个事务执行了两遍写操作。MongoDB连接串方面注意不要设置过小的maxPoolSize因为MongoDB事务期间连接是独占的如果连接池过小并发一上来就会因为获取不到连接而超时。推荐设置为100以上具体取决于你的服务部署实例数和并发量。9. 用一个完整的故障处理案例串起整个思路最后分享一个我之前处理过的线上故障把这个问题的完整排查和解决过程走一遍。9.1 故障现象某天下午运营反馈后台订单列表里有部分订单的详情页打开后日志区域是空的。这些订单的MySQL记录状态是CREATED但MongoDB里没有对应的订单日志。运营怀疑系统丢了数据要求排查。9.2 排查过程第一步我先查了应用日志。发现在订单创建接口的MongoDB写入处有报错MongoTimeoutException: Timed out after 30000 ms。这个报错信息表明MongoDB写操作超时了。第二步我查了MongoDB的监控面板发现当时MongoDB的CPU使用率达到了90%慢查询明显增多。进一步排查发现有个后台统计任务在MongoDB上跑了一个大范围的聚合查询占用了大量系统资源。第三步我确认了代码逻辑MongoDB写入是在Spring事务的afterCommit回调中执行的。虽然MongoDB写入失败了代码也执行了补偿逻辑把MySQL订单状态标记为LOG_WRITE_FAILED但运营看到的那批订单状态还是CREATED。原因是——补偿逻辑只处理了单条数据但那个时间段因为MongoDB超时补偿逻辑本身也发生了超时结果导致订单状态根本没来得及更新。9.3 解决方案我做了两件事第一把补偿逻辑改为异步执行。MongoDB写入失败后把orderId丢进一个本地内存队列或者直接业务库建一张失败重试表由单独的线程池负责重试重试次数上限为3次。这样即使第一次补偿失败后面还有机会。第二为MongoDB的数据一致性增加了一个对账任务每10分钟扫描一次失败记录把缺失的日志补写进去。这个故障的根本原因其实就是跨库操作没有兜底机制。MySQL那边事务很干净地提交了MongoDB这边因为外部因素失败了最后全靠补偿逻辑硬抗。一旦补偿逻辑也出问题数据一致性就破了。所以事务补偿对账三层缺一不可。10. 我的经验总结与建议做了这么多跨库事务的项目我最直观的体会是不要在架构层面追求不切实际的强一致要在设计层面尽量规避跨库操作。如果你确实无法避免一个接口同时操作MySQL和MongoDB那就按照这个优先级来做决策能异步的就异步。非核心数据、日志类数据、用户行为数据全部走MQ异步写入接口本身只操作MySQL。这是最简单、最稳妥的方案。不能异步的优先考虑MySQL先行 补偿逻辑的模式并把补偿逻辑做成幂等、可重试的。两个库都需要强一致的场景要回到业务层面问一问这两个数据源真的需要同时更新吗能不能把数据合并到一个库里还有一个小技巧写代码时把所有跨库操作集中在独立的Service方法中不要散落到业务代码各处。比如专门写一个OrderCrossStoreService统一管理先写MySQL再写MongoDB的编排和补偿。这样即使出了问题排查范围也小很多。我开发过程中最常提醒自己的三句话一个接口只保一个强事务另一个数据库是追求最终一致。每一条补偿逻辑都必须能重试、可幂等否则就是在给自己埋雷。监控和告警不是可选项是分布式事务方案的标配。这些经验听起来朴素但对线上稳定性至关重要。跨库事务问题从来不是写几行代码就能一劳永逸的它是一个需要从设计、编码、监控、运维多角度持续完善的系统工程。