分布式事务到底该怎么搞?一个秋招狗的血泪总结
上周五晚上十一点,我瘫在电竞椅上盯着屏幕上一行红色报错:XA transaction timeout。那一刻真的想拔电源——这已经是本周第三次因为订单支付状态不一致被测试追着问了。作为一个985计算机大三狗,正在远程准备秋招,白天刷LeetCode、晚上改bug,结果分布式事务这个坎儿愣是卡了我两周。
事情起因是我们组在重构一个电商后端系统。前端用React写得飞快(毕竟有ChatGPT帮我补UI逻辑),后端Java微服务拆得七零八落,最近还掺了点Go写的库存服务。产品经理拍脑袋说“要保证下单、扣库存、发券原子性”,运维大哥冷笑一声:“你们自己搞分布式事务吧,别半夜call我。”
于是,我被迫从“只会写CRUD”的学生仔,硬生生卷进了分布式事务的深水区。
为啥本地事务不够用了?
单体时代,一个@Transactional注解搞定一切。但微服务一拆,订单在Java服务A,库存跑在Go服务B,优惠券又在另一个Java服务C——三个数据库,三个进程,本地事务直接歇菜。
最开始我天真地用“先调A再调B再调C + 重试”这种脚本式逻辑,结果双11压测时出现大量超卖和状态不一致。测试小哥甩给我一张表:
| 场景 | 订单状态 | 库存 | 用户优惠券 |
|---|---|---|---|
| 正常流程 | 已支付 | -1 | 已发放 |
| 网络超时(库存服务挂) | 已支付 | 未扣 | 未发放 ❌ |
| 库存不足回滚失败 | 已取消 | -1 ❌ | 未发放 |
看到那个❌,我血压直接拉满。
主流方案横向对比:别再死记面试题了
网上一堆“分布式事务四大模式”的面试题,但真到生产环境,光背概念根本不够。结合我们项目,我试了三种主流方案:
1. 2PC(两阶段提交)——理论很美,现实很骨感
Java这边用Atomikos或Seata AT模式,理论上能跨DB协调。但实测发现:
- 性能差到离谱:一次下单TPS从300+掉到60
- 阻塞严重:某个服务宕机,整个事务卡住,连接池爆满
- 不支持异构语言:Go服务根本没法接入JTA
结论:只适合强一致性要求极高、且全是Java生态的小规模系统。我们果断放弃。
2. TCC(Try-Confirm-Cancel)——灵活但开发成本爆炸
TCC要求每个服务实现三套接口。比如库存服务:
// Try: 预占库存
func TryReserveStock(skuId string, num int) error { ... }
// Confirm: 真正扣减
func ConfirmStock(skuId string, num int) error { ... }
// Cancel: 释放预占
func CancelStock(skuId string, num int) error { ... }
听起来很优雅?但现实是:
- 每个业务都要写三倍代码
- 幂等性、空回滚、悬挂问题全得自己处理
- React前端还得配合做状态轮询(用户看到“处理中…”转圈5秒)
我们团队就3个后端,deadline还剩两周,TCC直接pass。
3. 最终一致性 + 消息队列 —— 我们的选择
既然强一致代价太高,那就退而求其次,用可靠消息最终一致性。核心思路:把分布式事务拆成本地事务 + 异步补偿。
具体落地:
- 订单服务创建订单(本地事务),同时发一条
OrderCreated消息到Kafka - 库存服务消费消息,扣库存(本地事务)
- 如果扣库存失败,发
CompensateOrder消息回订单服务,把订单置为失败
关键点在于消息可靠性:
- Java服务用Spring Cloud Stream + Kafka事务消息
- Go服务用
sarama库手动ACK + 重试机制 - 所有消息加唯一ID防重
// Java 发送可靠消息示例
@Transactional
public void createOrder(Order order) {
orderRepo.save(order);
// 本地事务成功后再发消息
messageSender.send("order-created", order.getId(), order);
}
这套方案上线后,TPS回到280+,不一致率<0.01%(通过定时对账补偿)。
架构设计中的几个魔鬼细节
幂等性不是可选项,是保命符
网络抖动、消息重复投递太常见了。我们在所有服务入口加了幂等校验:
// Go 服务幂等中间件
func Idempotent(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if cache.Exists(id) {
// 直接返回上次结果
return
}
cache.Set(id, true, 10*time.Minute)
next(w, r)
}
}
对账系统:兜底的最后一道防线
再完美的设计也会翻车。我们每天凌晨跑一个对账Job:
- 比对订单表 vs 库存流水 vs 优惠券记录
- 自动修复不一致数据
- 告警人工介入
这玩意儿救了我们好几次。有一次Kafka堆积导致消息延迟6小时,全靠对账捞回来。
别让前端瞎猜状态
React前端不能傻等!我们设计了状态机+主动查询:
- 下单后立即返回“处理中”
- 前端每2秒轮询订单状态
- 后端提供
/order/{id}/status接口(带缓存)
用户体验好了,客诉少了,产品经理终于不找我麻烦了。
秋招视角:面试官到底想听什么?
最近面了几家大厂,分布式事务几乎是必问题。我发现光说Seata/TCC原理已经不够了,面试官更想听:
- 你遇到过什么真实不一致场景?
- 补偿机制怎么设计的?
- 性能和一致性如何权衡?
比如我讲Kafka消息方案时,特意提到:
“我们接受短暂不一致,但通过幂等+对账保证最终一致,TPS提升4倍,业务方也认可这个trade-off。”
面试官眼睛明显亮了。
写在最后
从被分布式事务虐到能输出最佳实践,我最大的感悟是:没有银弹,只有权衡。学生时代总想找“完美方案”,工作后才明白,工程就是不断在一致性、可用性、复杂度之间找平衡点。
现在我的简历上终于能写“主导设计高可用分布式事务方案”了(笑)。如果你也在秋招路上挣扎,记住:踩过的坑,都是面试的谈资。
哦对了,今晚还要改一个Go服务的幂等bug…… 谁借我个键盘砸一下?

评论 0