分布式事务到底该怎么搞?一个秋招狗的血泪总结

程序员阿远
2025-12-29 06:31
阅读 246

上周五晚上十一点,我瘫在电竞椅上盯着屏幕上一行红色报错: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. 最终一致性 + 消息队列 —— 我们的选择

既然强一致代价太高,那就退而求其次,用可靠消息最终一致性。核心思路:把分布式事务拆成本地事务 + 异步补偿

具体落地:

  1. 订单服务创建订单(本地事务),同时发一条OrderCreated消息到Kafka
  2. 库存服务消费消息,扣库存(本地事务)
  3. 如果扣库存失败,发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

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