做后端这几年,我发现工程师有一个通病:看到一个新框架,Demo 跑通了,文档写得漂亮,Star 数够多,就觉得找到救星了。然后拍板,接入,上线。等到生产环境出问题,才开始真正读懂那份文档。Seata 就是我交的那笔学费。
我用了它 18 个月,然后亲手把它删了。
说实话,我到现在还记得当时接入 Seata 时候的心情。
那是 2022 年初,我们订单系统刚拆完微服务,下单链路横跨订单、库存、积分三个服务。每次开会讨论数据一致性问题,我都头大——要么逻辑复杂,要么性能差,要么代码写出来新人根本看不懂。
然后我发现了 Seata。
加一个@GlobalTransactional注解,框架自动搞定分布式事务,对业务代码完全透明。我当时的第一反应是:这不就是我一直在找的东西吗?
Demo 跑起来,十分钟。那种感觉,就像在黑暗里突然摸到了开关。
我把文档甩给团队,说就用这个了。
Seata 是怎么工作的
在讲踩坑之前,先把原理说清楚,不然后面的问题看不懂。
Seata 有三个角色:TM(事务发起方)、RM(各服务的资源管理器)、TC(独立部署的事务协调者)。
它的 AT 模式是这样运作的:每次你执行一条 SQL,Seata 会在执行前后各拍一张数据快照,存到undo_log表里。如果事务需要回滚,就用这张快照把数据还原。
听起来很聪明。但有个细节藏在这套流程里,很多人接入的时候没注意——
在全局事务提交之前,被改动的那行数据会被加上一把全局锁。
第一笔债:压测的时候还的
上线前做全链路压测,QPS 跑到 500,DBA 打电话过来,语气不太好:数据库锁等待堆了一大堆,问我们在搞什么。
拉出监控,订单接口 P99 从 80ms 涨到了 640ms。
排查了半天才搞清楚:热门商品的库存那一行,500 个并发请求都在抢同一把全局锁。每个请求还得和 TC 额外通信一次,网络来回大概 20ms。P99 不炸才怪。
这个细节文档里有写,我当时没当回事。后来我跟几个朋友聊,发现大家都是这么踩过来的。Demo 没有并发,感受不到。
压测 QPS | P99 延迟 | DB 锁等待 | 结论 |
|---|---|---|---|
100 | 82ms | 无 | ✅ 正常 |
300 | 160ms | 偶发 | ⚠️ 有点抖 |
500 | 640ms | 大量 | ❌ 报警 |
800 | 超时 | 死锁 | ❌ 熔断 |
从一个坑跳进另一个坑
我当时的应对方案是换 TCC 模式。TCC 没有全局锁,性能确实好很多。
代价是你要自己实现三个接口:Try、Confirm、Cancel。
我让小陈负责改造,两周后他提了 PR,我去 review,看到 Cancel 接口有点懵——里面有一段逻辑我没看懂,问他这是干嘛的。
他解释了一通,我才知道这叫「空回滚」——Try 还没执行,Cancel 先到了,你得处理这种情况。除此之外还有幂等、悬挂,每一个接口都要把这几种异常场景全部考虑进去。
我问他:每个接口都要这么写?
他说:对。
我们有三个服务,加起来十七个接口。
我默默关掉了 PR 页面,坐在那想了一会儿。
这不是我想要的方向。我当初选 Seata 就是因为它不侵入业务,结果 TCC 把我推回了那个更深的坑里。
TC,一个藏在架构里的定时炸弹
TC 是 Seata 的事务协调者,独立部署,所有全局事务都要跟它通信。
11 月的某个周三下午 3 点,线上突然大量报警。和以往不同,这次不是慢,是所有跨服务的事务全部挂起。
原因是 TC 集群有个节点网络抖动,持续了 30 秒。
就这 30 秒,订单接口超时,连接池打满,用户下单失败。故障持续了将近 4 分钟。
复盘的时候我在白板上画了一张图,把 TC 圈出来,才意识到一件事:我在核心链路上引入了一个全局单点,它一旦出问题,影响面是所有业务。
那张让我下定决心的截图
DBA 某天下午发来一张图,什么话没说。
undo_log | 18,432 MB18G。还在涨。
AT 模式高并发下,全局锁让事务变慢,慢事务让undo_log积压,清理任务根本追不上写入速度。这张表就悄悄长到了 18G,没人注意,直到 DBA 发现。
凌晨 1 点 47 分
钉钉报警群连续弹了十几条消息。
P99 = 2300ms,全局锁死锁。三个月内第三次了。
我拉着几个人语音,盯着那条红线看了大概二十秒。
我说:摘了吧。
没人反对。包括当初力推 Seata 的人,也就是我自己。
删掉之后,我们用的是这个
说出来一点都不新鲜:本地消息表 + MQ + 幂等消费。
核心思路是这张图:
下单的时候,在同一个本地事务里写订单、同时写一条「待发消息」进 outbox 表。事务提交,这两条数据必然同时存在;事务回滚,两条一起没。原子性天然保证,不需要任何框架。
然后有个独立进程轮询 outbox 表,把消息投到 RocketMQ。下游消费,幂等处理,失败了 MQ 重试,超限进死信队列,告警,人工处理。
就这样。
上线一个月,订单 P99 从 640ms 降到 68ms,CPU 峰值砍了一半,undo_log那张表连结构都删了。
小陈看完方案沉默了三秒,说了一句:就这?
对,就这。
所以 Seata 到底适不适合用
我现在回头看,觉得当时最大的问题不是选错了工具,是想用一个框架把分布式事务这件事完全封装掉。这个想法本身就不现实。
强一致性分布式事务本身就很贵,没有框架能消灭这个矛盾,只能把复杂度转移到某个地方——Seata 把它转移到了全局锁、TC 和 undo_log,你最终还是要承担。
这些场景用 Seata 是合适的:
并发量不高,峰值 QPS < 100
业务必须强一致,比如金融账务、票务核销
团队有人专职维护 TC 集群
这些场景建议用消息方案:
高并发互联网业务,QPS 500+
有热点数据竞争,比如秒杀、热门商品库存
团队小、迭代快、没人专职维护
消息队列加本地消息表,没有花哨的概念,新来的同学一天就能看懂。
这才是我们想要的架构。
如果你也踩过类似的坑,评论区聊聊。这种事情,说出来总比憋着强。