分布式事务解决方案:我在大厂踩过的坑和攒下的经验
写这篇文章的时候,是凌晨两点。窗外的上海早就安静下来了,只有楼下车库偶尔传来几声电动车报警的哀鸣。我刚泡好一杯速溶咖啡(别笑,真没精力手冲了),打开 IDE,准备把最近几个月折腾分布式事务的心得整理出来。
没错,我已经从某“福报厂”辞职了——不是因为被裁,也不是因为 PUA,纯粹是想停下来想想自己到底要干嘛。毕竟连续两年在双11前夜通宵改 bug、被产品经理按着头加需求、还要应付各种“高可用、高性能、高并发”的 PPT 架构图,真的有点心累。
但技术这东西吧,一旦入了坑就很难彻底放下。上周刷招聘软件看 Java 岗位,发现几乎所有中高级后端岗都写着“熟悉分布式事务”、“有 Seata/TCC 实践经验优先”。我心想:简历上要是只写“了解 CAP 理论”,怕是要被 HR 当成野生程序员筛掉。于是干脆趁着这段“gap time”,把之前项目里踩过的雷、熬过的夜、骂过的中间件,全都翻出来复盘一遍。
事情是怎么开始的?
去年我们团队接了个新业务:要做一个“虚拟商品+实物发货”的混合订单系统。用户买个课程还能顺便带一箱纸巾,听起来很美好对吧?但问题来了——课程库存走的是 MySQL + Redis 的本地事务,而实物库存对接的是第三方 WMS(仓储管理系统),走的是 HTTP 接口。
这就尴尬了:下单成功但发不了货?或者发了货但课程没到账?用户投诉起来分分钟上热搜。更惨的是,产品经理还说“这个需求很简单,下周上线”,而测试同学已经在群里@我说:“你们这个事务不一致的问题,上次压测已经崩了三次了。”
当时我就坐在工位上盯着屏幕发呆,脑子里全是“XA”、“TCC”、“Saga”这些词在打架。最后咬牙决定:不能靠 try-catch 和人工补单撑下去了,必须上正经的分布式事务方案。
方案选型:不是所有“高大上”都适合你
市面上主流的分布式事务方案大概就那么几种:
| 方案 | 一致性级别 | 性能影响 | 开发复杂度 | 适用场景 |
|---|---|---|---|---|
| XA 两阶段提交 | 强一致 | 高(锁表时间长) | 中 | 数据库同构、低并发核心交易 |
| TCC | 最终一致 | 中(需预留资源) | 高 | 金融、支付等关键链路 |
| Saga | 最终一致 | 低 | 中 | 长流程、异步补偿场景 |
| 本地消息表 | 最终一致 | 低 | 低 | 简单业务、容忍延迟 |
| Seata(AT模式) | 最终一致 | 中 | 低 | 快速落地、微服务架构 |
我们评估了一下:业务量不算特别大(日订单 10w+),但要求不能丢数据,且开发人力紧张。最后选了 Seata 的 AT 模式——它最大的优点是侵入性小,基本不用改业务代码,只需要加几个注解,再配个 TC(Transaction Coordinator)服务就行。
当然,这也意味着我们要交“学费”:后来才知道 Seata 在高并发下 GC 压力很大,而且对数据库连接池特别敏感。不过那是后话了。
踩坑实录:线上炸了三次才调明白
坑1:全局锁和死锁
第一次上线是在周三晚上,自以为万事俱备。结果第二天早上就被告警电话吵醒:“订单创建失败率飙升到 30%!”
查日志发现全是 Global lock wait timeout。原来 Seata 的 AT 模式会在 undo_log 表里记录行级快照,并在提交前加全局锁。而我们的订单表和库存表都在同一个 DB 实例里,多个微服务同时操作时,锁竞争直接爆炸。
解决办法:
- 把 undo_log 表单独建在一个轻量级 DB(比如 TiDB)里,避免和业务表争抢 InnoDB 锁
- 调整
lock.retry.times和lock.retry.interval参数,让重试更平滑
# seata.conf 关键配置
client:
rm:
lock:
retry:
times: 30 # 默认10次,太少了
interval: 20 # 单位ms,别设太小
坑2:回滚失败导致数据不一致
有一次用户反馈:“钱扣了,课没到账。” 查了 Seata 控制台,发现全局事务状态是 Rollbacked,但业务库里的订单状态还是“已支付”。
原来是我们写的 @GlobalTransactional 方法里,catch 了异常但没 rethrow!Seata 默认只有在抛出 RuntimeException 时才会触发回滚。我们为了“优雅处理错误”,默默吞掉了异常,结果事务管理器以为一切正常……
教训:要么别 catch,要么 catch 之后 throw new RuntimeException。别自作聪明!
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public void createOrder(OrderRequest request) {
try {
// 扣课程库存
courseService.decreaseStock(request.getCourseId());
// 调用WMS创建发货单
wmsClient.createShipment(request.getGoodsList());
// 保存订单
orderRepo.save(buildOrder(request));
} catch (Exception e) {
log.error("订单创建失败", e);
throw new RuntimeException("订单创建失败,请重试", e); // ⚠️ 必须 rethrow!
}
}
坟3:性能瓶颈在数据库连接池
压测时发现,当并发超过 500,Seata 的 RM(Resource Manager)频繁报 getConnection timeout。追源码才发现:Seata 在执行分支事务时,会为每个参与方单独申请一个 DB 连接。而我们用的 HikariCP 连接池 maxPoolSize 才 20。
临时方案:把连接池调到 100。
长期方案:拆库!把订单、课程、用户分到不同数据源,减少单点压力。
性能优化:别只顾功能,忘了 QPS
分布式事务天生有开销,但我们也不能让它拖垮整个系统。分享几个实战优化点:
- 避免大事务:一个 @GlobalTransactional 方法里别塞太多操作。我们曾经在一个事务里调了 8 个服务,结果超时率高达 15%。后来拆成两个阶段:先锁定资源(TCC 预留),再异步确认。
- 异步化非核心操作:比如发短信、写日志,不要放在全局事务里。用 RocketMQ 事务消息兜底就行。
- 监控必不可少:我们用 Prometheus + Grafana 监控了 Seata 的事务成功率、平均耗时、回滚率。一旦回滚率 > 0.5%,立刻告警。
如果重来一次,我会怎么选?
说实话,如果现在让我重新设计这个系统,我可能会放弃 Seata,转而用 Saga + 本地消息表 的组合。
为什么?因为我们的业务其实容忍一定延迟(用户等几分钟拿到课程也能接受),而 Saga 的补偿逻辑更清晰,运维也更简单。Seata 虽然“开箱即用”,但一旦出问题,排查成本极高——你得同时看 TC 日志、RM 日志、业务日志,还得懂它的协议栈。
而且,很多公司根本不敢在线上核心链路用 Seata。我前同事跳槽去某券商,他们连 Redis 都不敢用,更别说开源分布式事务框架了,全是自己撸的 TCC。
给正在看这篇文章的你:几点建议
- 别为了简历好看硬上分布式事务。如果你的系统还没到跨库跨服务的程度,老老实实用本地事务 + 重试机制 + 人工对账,反而更稳。
- 测试一定要覆盖“网络抖动”场景。我们用 ChaosBlade 模拟过 TC 宕机、RM 超时、DB 主从切换,这些才是真实世界的常态。
- 文档比代码重要。每次上线新事务流程,我都逼自己写清楚:哪些步骤可回滚?补偿接口怎么调?谁负责兜底?不然半夜 oncall 的时候你会哭。
写在最后
辞职这一个月,我反而比上班时更认真地学技术。没有 KPI 压着,没有 PRD 催着,终于能静下心来看看源码、跑跑 demo、写写总结。
分布式事务这东西,说白了就是“用复杂性换一致性”。没有银弹,只有权衡。你在简历上写“精通 Seata”,不如写清楚“在什么场景下用了什么方案,解决了什么问题,QPS 从多少提升到多少”。
毕竟,面试官要的不是一个会背概念的人,而是一个能在凌晨三点冷静处理线上事故的靠谱后端。
好了,咖啡见底了,天也快亮了。希望这篇带点血泪的经验,能帮你少熬几个夜。
(P.S. 如果你也在上海找工作,欢迎私信聊聊。虽然我现在在 gap,但认识不少靠谱团队,Java 后端岗位多得很——前提是你的简历别只写“熟悉分布式事务” 😏)

评论 0