分布式事务解决方案:我在大厂踩过的坑和攒下的经验

代码不眠人
2025-12-15 17:44
阅读 202

写这篇文章的时候,是凌晨两点。窗外的上海早就安静下来了,只有楼下车库偶尔传来几声电动车报警的哀鸣。我刚泡好一杯速溶咖啡(别笑,真没精力手冲了),打开 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.timeslock.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

分布式事务天生有开销,但我们也不能让它拖垮整个系统。分享几个实战优化点:

  1. 避免大事务:一个 @GlobalTransactional 方法里别塞太多操作。我们曾经在一个事务里调了 8 个服务,结果超时率高达 15%。后来拆成两个阶段:先锁定资源(TCC 预留),再异步确认。
  2. 异步化非核心操作:比如发短信、写日志,不要放在全局事务里。用 RocketMQ 事务消息兜底就行。
  3. 监控必不可少:我们用 Prometheus + Grafana 监控了 Seata 的事务成功率、平均耗时、回滚率。一旦回滚率 > 0.5%,立刻告警。

如果重来一次,我会怎么选?

说实话,如果现在让我重新设计这个系统,我可能会放弃 Seata,转而用 Saga + 本地消息表 的组合。

为什么?因为我们的业务其实容忍一定延迟(用户等几分钟拿到课程也能接受),而 Saga 的补偿逻辑更清晰,运维也更简单。Seata 虽然“开箱即用”,但一旦出问题,排查成本极高——你得同时看 TC 日志、RM 日志、业务日志,还得懂它的协议栈。

而且,很多公司根本不敢在线上核心链路用 Seata。我前同事跳槽去某券商,他们连 Redis 都不敢用,更别说开源分布式事务框架了,全是自己撸的 TCC。


给正在看这篇文章的你:几点建议

  1. 别为了简历好看硬上分布式事务。如果你的系统还没到跨库跨服务的程度,老老实实用本地事务 + 重试机制 + 人工对账,反而更稳。
  2. 测试一定要覆盖“网络抖动”场景。我们用 ChaosBlade 模拟过 TC 宕机、RM 超时、DB 主从切换,这些才是真实世界的常态。
  3. 文档比代码重要。每次上线新事务流程,我都逼自己写清楚:哪些步骤可回滚?补偿接口怎么调?谁负责兜底?不然半夜 oncall 的时候你会哭。

写在最后

辞职这一个月,我反而比上班时更认真地学技术。没有 KPI 压着,没有 PRD 催着,终于能静下心来看看源码、跑跑 demo、写写总结。

分布式事务这东西,说白了就是“用复杂性换一致性”。没有银弹,只有权衡。你在简历上写“精通 Seata”,不如写清楚“在什么场景下用了什么方案,解决了什么问题,QPS 从多少提升到多少”。

毕竟,面试官要的不是一个会背概念的人,而是一个能在凌晨三点冷静处理线上事故的靠谱后端。

好了,咖啡见底了,天也快亮了。希望这篇带点血泪的经验,能帮你少熬几个夜。

(P.S. 如果你也在上海找工作,欢迎私信聊聊。虽然我现在在 gap,但认识不少靠谱团队,Java 后端岗位多得很——前提是你的简历别只写“熟悉分布式事务” 😏)

评论 0

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