分布式事务解决方案:最佳实践

活泼之网络
2025-12-19 14:23
阅读 496

上周五晚上,我正蹲在深圳科技园某大厂的工位上,一边啃着冷掉的猪脚饭,一边盯着屏幕里一串诡异的“数据不一致”告警。这已经是本周第三次了。产品那边还在群里@我们:“这个月必须上线新订单系统,双11前要是出问题,你们就别过年了。”

作为团队里那个“写代码还管架构”的人(其实只是因为没人愿意干),我不得不重新审视我们的分布式事务方案。说白了,就是跨服务、跨数据库的那点破事——用户下单成功了,但库存没扣;支付成功了,积分却没到账。这种 bug 放线上,轻则客诉,重则被老板请去喝茶。


为什么我又翻出了分布式事务这本“老黄历”?

其实这事我去年就踩过坑。当时项目用的是纯 Spring Boot + MySQL,单体架构跑得飞起。后来业务一膨胀,领导一句“微服务化”,硬是把一个模块拆成了五个服务:订单、库存、账户、积分、物流。结果一上线,用户反馈“付了钱没发货”,运维兄弟半夜打我电话:“兄弟,你代码又炸了。”

那会儿我们用的是最原始的手动补偿:先扣库存,再创建订单,再调支付……哪一步失败,就手动回滚前面的操作。听起来很“优雅”,实际上就是一堆 try-catch 套娃,逻辑绕得像深圳北站的地铁换乘图。更惨的是,一旦某个服务宕机,整个流程就卡住,数据处于“薛定顿状态”——既没成功也没失败。

痛定思痛,我决定系统性地搞清楚:在 Java 生态里,到底有哪些靠谱的分布式事务方案?它们各自适合什么场景?

于是,我花了两周时间,把市面上主流的方案都撸了一遍——从两阶段提交(2PC)到 TCC,再到 Saga、本地消息表、RocketMQ 事务消息,甚至试了 Seata。期间还拉着隔壁腾讯系公司的朋友喝了三次茶(其实是蹭他们的会议室),交流了不少血泪经验。

今天这篇,不讲理论八股,只聊真实项目中的选型、踩坑和开发心得。毕竟,咱们程序员最讨厌纸上谈兵,对吧?


方案对比:不是越新越好,而是越稳越香

先上一张我整理的对比表,这是我在 Cursor 里边调试边总结的(没错,现在我基本只用 Cursor 写代码了,后面细说):

方案 强一致性 性能 实现复杂度 回滚能力 适用场景
2PC (XA) ❌ 低 单 DB 多资源,如 JTA
TCC ✅ 高 ❌ 高 ✅(需自定义) 金融、高一致性要求
Saga ❌ 最终一致 ✅ 高 ❌(靠补偿) 长流程、业务可逆
本地消息表 ❌ 最终一致 ✅(重试+补偿) 简单异步解耦
RocketMQ 事务消息 ❌ 最终一致 ✅ 高 ✅(靠回查) 已用 MQ 的系统
Seata AT 模式 ⚠️ 中 快速接入,中小项目

注:✅ 表示支持良好,❌ 表示不支持或效果差,⚠️ 表示有条件支持

1. 2PC:理论很美,现实很骨感

Java 里可以用 Atomikos 或 Bitronix 实现 JTA,配合 XA 协议做两阶段提交。听起来很“ACID”,但实际用起来简直折磨。

  • 性能瓶颈:协调者要等所有参与者响应,锁持有时间长,高并发下直接雪崩。
  • 单点故障:协调者挂了,整个事务就僵死。
  • DB 支持有限:MySQL 虽然支持 XA,但官方文档都写着“不推荐生产使用”。

我们试过一次,结果压测时 TPS 直接从 3000 掉到 200,DBA 在群里骂了一下午。从此,2PC 被我们打入冷宫。

2. TCC:灵活但累死人

TCC(Try-Confirm-Cancel)思路很清晰:先预留资源(Try),再确认(Confirm)或取消(Cancel)。比如:

  • Try:冻结库存
  • Confirm:扣减冻结库存
  • Cancel:释放冻结库存

优点:性能好,无长时间锁,支持强一致性。
缺点:每个业务都要写三套逻辑,代码量爆炸。而且,Cancel 不一定能完全回滚(比如已经发了短信)。

我们有一个支付核心模块用了 TCC,结果每次改需求,测试同学都要哭:“你们这 Confirm 和 Cancel 是不是又没对齐?” 后来我们干脆写了自动化校验脚本,确保三段逻辑原子性。

如果你团队有足够人力、且业务对一致性要求极高(比如银行转账),TCC 值得考虑。否则,慎入。

3. Saga:长流程的救星,但补偿逻辑容易成“俄罗斯套娃”

Saga 本质是一串本地事务 + 补偿操作。比如:

  1. 创建订单 → 失败?跳 5
  2. 扣库存 → 失败?执行补偿1(删订单)
  3. 扣余额 → 失败?执行补偿2(回补库存)、补偿1
  4. 发积分 → ……

优点:无锁、高性能、适合长流程(比如电商下单+风控+物流调度)。
缺点:补偿逻辑复杂,且无法保证隔离性——中间状态可能被其他请求读到(比如用户看到“订单已创建但库存未扣”)。

我们现在的主站订单链路就是 Saga + 状态机实现的。用 State Machine 来管理流程,避免 if-else 地狱。但说实话,第一次上线时因为补偿顺序写反了,导致几百个用户“白嫖”了商品……那次事故后,我们加了全链路 mock 测试,才敢放量。

4. 本地消息表:土但稳

这是最接地气的方案:在业务库建一张 message_outbox 表,事务内同时写业务数据和消息记录,再由定时任务或 MQ 消费器异步发送。

-- 订单服务
BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO message_outbox (msg_id, topic, payload, status) 
VALUES ('xxx', 'order_created', '{"orderId":"123"}', 'pending');
COMMIT;

然后有个后台任务轮询这张表,发 MQ 成功后更新状态。

优点:简单、可靠、不依赖外部中间件。
缺点:需要额外表、轮询有延迟、消息可能重复(需幂等)。

我们在一些非核心链路(比如发通知、打日志)用这套,跑了两年没出过大问题。虽然“土”,但在深圳这种“快速迭代、稳定压倒一切”的环境里,反而最受欢迎。

5. RocketMQ 事务消息:已有 MQ 的首选

如果你已经在用 RocketMQ,那它的事务消息简直是天选之子。

原理是:先发“half message”,执行本地事务,再根据结果 commit 或 rollback。Broker 会定期回查本地事务状态。

TransactionMQProducer producer = new TransactionMQProducer("group");
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // 执行本地事务,比如扣库存
        return LocalTransactionState.COMMIT_MESSAGE;
    }
    
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查:检查本地事务是否成功
        return orderService.isOrderCreated(msg.getKeys()) ? 
               COMMIT_MESSAGE : ROLLBACK_MESSAGE;
    }
});

优点:解耦好、性能高、社区成熟。
缺点:依赖 RocketMQ,回查机制增加复杂度,且只能保证最终一致。

我们现在的积分系统就用这个,配合幂等设计,基本没出过问题。唯一吐槽是:回查接口的幂等性一定要做好,不然 Broker 反复调,数据库直接被打爆。

6. Seata:快速落地的“银弹”?

Seata 的 AT 模式号称“零侵入”:通过代理数据源,在 SQL 执行前后生成 undo log,全局事务失败时自动回滚。

# application.yml
seata:
  enabled: true
  tx-service-group: my_tx_group

加上注解 @GlobalTransactional 就行。

优点:接入快,对业务代码几乎无改动。
缺点:性能损耗明显(每条 SQL 都要记录 undo log),高并发下 GC 压力大;且依赖 Seata Server,又是单点隐患。

我们试过在一个内部工具项目用 Seata,确实快。但放到核心交易链路?DBA 直接否了:“你看看这 CPU 使用率,再看看这 Full GC 频率……”


我的选择:混合架构 + 最终一致

经过这么多折腾,我们现在采用的是 “核心链路 TCC + 边缘链路 RocketMQ 事务消息 + 状态机驱动” 的混合架构。

  • 下单主流程:TCC(订单、库存、账户)
  • 积分、通知、日志:RocketMQ 事务消息
  • 全流程状态管理:基于 Spring StateMachine 的 Saga 编排

为什么不用单一方案?因为没有银弹。业务场景千奇百怪,强行统一只会把自己绕进去。

另外,无论哪种方案,以下几点必须做到:

  1. 幂等性:所有接口必须支持重复调用(用唯一 ID + DB 唯一键)
  2. 可观测性:全链路 trace + 事务状态监控(我们用 SkyWalking + 自研看板)
  3. 人工干预通道:万一自动补偿失败,要有后台手动修复入口(别笑,真的救过命)

开发心得:工具真的能改变效率

说到这儿,不得不提一句:这次重构,我全程用 Cursor 写代码。之前试过 GitHub Copilot、CodeWhisperer、通义灵码,但最终还是回到了 Cursor。

为什么?因为它不只是“补全代码”,而是能理解上下文、对话式调试、自动写单元测试。比如我写 TCC 的 Cancel 方法时,它直接提醒:“你这里没处理库存超卖,要不要加个版本号乐观锁?” —— 这种 level 的提示,其他工具真做不到。

而且,它还能帮我生成 Seata 的配置模板、RocketMQ 的事务监听器骨架,甚至自动生成补偿逻辑的测试用例。以前写这些要半天,现在喝杯瑞幸的功夫就搞定了。

在深圳这种卷成麻花的技术圈,工具链的效率直接决定你能不能准点下班。信我,试试 Cursor,你会回来谢我。


最后:分布式事务的本质,是业务妥协

写到最后,想说句掏心窝子的话:分布式事务不是技术问题,而是业务权衡问题

你不可能既要强一致性,又要高并发,还要零成本。产品经理嘴里的“必须实时一致”,在工程师眼里往往是“可以接受 5 秒延迟”。所以,沟通比编码更重要。

我们现在的策略是:和产品一起定义“可接受的不一致窗口”,然后选择最轻量的方案。比如积分到账延迟 30 秒?用户根本感知不到,但系统压力小了一半。

技术是为业务服务的,不是反过来。别为了炫技,把系统搞成“理论上完美,实际上天天报警”。

好了,猪脚饭凉了,该去修下一个 bug 了。希望这篇能帮你少踩几个坑。如果觉得有用,欢迎转发给你那个总想上 2PC 的同事 😏

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝