分布式事务解决方案:最佳实践(一个被房贷和地铁压垮的北漂程序员的血泪总结)
上周五晚上 10 点半,我拖着灌了铅的双腿从世纪大道地铁站走出来,手机震动了一下——是女朋友发来的消息:“饭热好了,你到哪了?”
我看了眼导航,离家还有 800 米。但此刻脑子里全是白天那个线上事务不一致的告警:用户付了钱,订单状态却还是“待支付”。老板在群里@我:“这都第几次了?再出问题,Q3 绩效直接打 C。”
我站在路口喘了口气,心里骂了一句:“分布式事务,你他妈能不能别再折磨我了?”
我是谁?一个被现实按在地上摩擦的 Java 程序员
先自我介绍一下吧。我是老张,坐标上海浦东,和女朋友合租在金桥的一个老小区,月租 3500(押一付三,每次交租前一周就开始焦虑)。每天早上 7:15 出门,挤 2 号线转 9 号线,单程 1 小时 10 分钟;晚上回来基本 9 点以后。月薪从 15k 涨到 22k 那天,我还请女朋友吃了顿人均 200 的日料——结果第二天就收到银行短信:“本月房贷还款 6843.21 元已扣款。”
我们做的系统是个电商中台,订单、库存、账户分三个微服务,全用 Spring Boot + MySQL 写的。去年十月上线后,事务问题就没断过。老板说:“你们搞技术的,不就是解决这种问题的吗?” 我心想:说得轻巧,你来写两行 TCC 试试?
被逼上梁山:从“我以为”到“我裂开了”
一开始,我和组里另一个兄弟小李天真地以为,加个 @Transactional 就万事大吉了。毕竟在学校、培训班、甚至 GitHub 上看的 demo,都是单体应用,事务回滚顺滑如德芙。
结果上线第一天,用户下单成功,库存没扣,钱却扣了。客服电话被打爆。CTO 在会议室拍桌子:“你们是不是没学过分布式事务?!”
我弱弱地说:“学过……但课本上没教怎么扛住 5000 TPS 啊。”
于是,我们被迫开始调研真正的分布式事务方案。以下是我踩过的坑和吐过的槽。
方案一:2PC(两阶段提交)—— 理论很美,现实很骨感
2PC 是教科书级方案,XA 协议那一套。MySQL 从 5.7 开始支持 XA 事务,理论上能保证强一致性。
实测结果?
- 吞吐量直接掉到 300 TPS,比单机还慢。
- 任何一个节点挂了,整个事务卡住,资源锁死。
- 运维大哥看到监控图直接冲进我们工位:“你们谁写的代码?数据库连接池快爆了!”
结论: 别碰。除非你公司有钱到能养一支 DBA 团队专门调优,或者业务量小到一天就几十单——那你还搞啥微服务?
方案二:TCC(Try-Confirm-Cancel)—— 强一致性,但代价巨大
TCC 是阿里开源 Seata 主推的模式。核心思想是:把业务逻辑拆成三步——先冻结资源(Try),再确认(Confirm),失败就取消(Cancel)。
听起来很牛,对吧?但落地的时候我差点辞职。
比如“扣库存”这个操作:
- Try:库存 -1,但状态设为“预占”
- Confirm:正式扣减
- Cancel:释放预占
问题来了:每个接口都要写三套逻辑! 而且要处理幂等、空回滚、悬挂等问题。我们光是“订单创建”一个流程,就写了 300 多行补偿代码。
更惨的是,有一次网络抖动,Confirm 请求丢了,系统以为失败了,其实库存已经扣了。用户投诉:“我付了两次钱,只收到一个货!”
GitHub 上搜 TCC,Star 最多的项目 issue 区里全是:“Cancel 没触发怎么办?”、“如何保证 Try 和 Confirm 的原子性?”
我的感受: TCC 能用,但成本太高。适合金融级场景,比如转账。但我们这种卖日用品的电商?ROI(投入产出比)太低。
方案三:本地消息表 + 最终一致性 —— 打工人之光
被 TCC 折磨一个月后,我在 GitHub 上翻到了一个叫 eventuate-tram 的项目(虽然现在不太维护了),灵感来自《微服务架构设计模式》这本书。
思路很简单:
- 在本地事务中,同时写业务数据 + 发送消息记录(比如“订单已创建”)
- 用定时任务或消息队列(比如 RabbitMQ/Kafka)异步消费这条消息,去调库存服务
- 如果失败,重试 N 次,直到成功
优点?
- 不依赖外部协调者,性能好(我们压测到 4000+ TPS)
- 代码侵入小,只需要加个消息表
- 失败可追溯,重试机制清晰
缺点?
- 数据不是实时一致,有延迟(用户可能看到“订单成功”但库存还没扣,几秒后才同步)
- 要自己处理消息重复(幂等!幂等!幂等!重要的事说三遍)
但对我们来说,最终一致性完全够用。用户又不是做高频交易,等 2 秒看到库存更新,根本无感。
方案四:Seata AT 模式 —— 阿里爸爸的“银弹”?
去年底,公司决定上 Seata。AT 模式号称“零代码改造”,自动解析 SQL 生成 undo log,像本地事务一样用。
我兴奋地熬了两个通宵集成,结果……
- Undo log 表膨胀得飞快,DBA 喊停
- 跨库查询直接报错
- 一旦主键不是自增 ID,回滚就乱套
最离谱的是,Seata Server 本身成了单点故障。有次它挂了,整个下单链路瘫痪 40 分钟。
GitHub 上 Seata 的 issue 区堪称大型吐槽现场:“AT 模式在生产环境真的能用吗?”、“undo_log 表怎么清理?”、“为什么回滚后数据还是脏的?”
我的结论: Seata 适合新项目、规范统一的团队。但我们这种历史包袱重、数据库设计五花八门的老系统?别硬上,容易翻车。
最终选择:本地消息表 + RocketMQ 事务消息
折腾半年后,我们定了最终方案:
- 核心链路(下单、支付)用 RocketMQ 事务消息
- 非核心(发券、积分)用 本地消息表 + 定时补偿
RocketMQ 的事务消息机制很巧妙:
- 先发 Half Message(半消息)
- 执行本地事务(比如扣账户余额)
- 成功则 Commit,失败则 Rollback
它内部用 checkpoint 机制保证消息不丢,而且支持 Exactly-Once 语义。虽然要写点回调逻辑,但比 TCC 简单多了。
关键是:稳定! 上线三个月,0 事务不一致事故。老板终于不再 @ 我了。
写给和我一样的普通程序员
我知道,很多人看到“分布式事务”就头大。网上教程动不动就“CAP 定理”、“BASE 理论”,搞得好像不用 TCC 或 Saga 就不配当架构师。
但现实是:大多数业务,根本不需要强一致性。
你月薪 22k,背 6800 的房贷,挤两小时地铁,不是为了实现学术理想,而是让系统稳稳跑起来,让用户别投诉,让自己能准点下班陪女朋友吃顿热饭。
所以我的建议很朴素:
- 先问业务容忍度:能接受几秒延迟?能接受少量人工干预吗?
- 优先选最终一致性:本地消息表 or 事务消息,简单可靠
- 别迷信大厂方案:阿里的 TCC 是为双 11 设计的,你可能连 11 都没有
- 善用 GitHub:别重复造轮子,但一定要读源码、看 issue,别光看 README
结尾:技术之外,生活才是终极事务
昨天晚上,女朋友看我还在改代码,叹了口气:“你什么时候能不加班啊?”
我说:“等我把这个分布式事务搞定。”
她笑了:“那你这辈子都别想准时回家了。”
我愣了一下,也笑了。
其实,人生何尝不是一场“最终一致性”的事务?
工作、爱情、房贷、梦想……它们永远不会在同一刻完美同步。
但只要不断重试、补偿、向前推进,终会达到一种动态的平衡。
而我们这些背着房贷的北漂程序员,不就是在这样的不一致中,努力寻找属于自己的“Commit”时刻吗?
P.S. 本文所有方案代码我都整理到了 GitHub:https://github.com/zhang-bj/distributed-tx-practice(别 star,没人维护,纯个人笔记)
如果你也正在被分布式事务折磨,欢迎留言。至少,我们还能互相骂一句:“这破系统,真难搞!”

评论 0