分布式事务怎么搞?我在快手踩过的坑和填坑工具箱

出色的守护者
2025-12-25 21:56
阅读 480

去年双11前两周,我们组负责的订单履约系统突然在压测时炸了——用户付了钱,库存扣了,但优惠券没核销。运维兄弟半夜拉群@我:“老张,又双叒叕数据不一致了!”那一刻我真想把电脑砸了,毕竟这已经是本月第三次因为分布式事务翻车了。

我是快手的老兵,在后端架构岗上干了快六年,从0到1搭过用户中心、支付网关、实时推荐引擎。这两年在履约组带团队,天天跟分布式事务“搏斗”。说实话,刚接手这个系统时,我对分布式事务的理解还停留在“两阶段提交”这种教科书概念上,直到被线上事故教育得体无完肤。

今天这篇文章,就把我这两年在Java生态里摸爬滚打总结出来的实战经验分享出来。不讲虚的理论,只聊能跑在生产环境里的工具最佳实践


为啥分布式事务这么难搞?

先说背景。我们的履约流程涉及至少四个服务:订单服务(写MySQL)、库存服务(写Redis+MySQL)、优惠券服务(写MongoDB)、风控服务(异步调用)。每个服务都是独立部署、独立数据库,典型的微服务架构。产品经理一句话:“用户下单必须原子成功”,结果我们后端就得保证跨库、跨服务的数据一致性。

早期我们尝试过最朴素的方案:本地事务 + 重试补偿。比如下单时先扣库存,再发MQ通知优惠券核销。但如果MQ挂了或者优惠券服务超时,就得靠定时任务扫表补偿。结果呢?测试同学提了200+个边界Case,光是“部分成功+重试+并发”组合就让我们加班到凌晨三点。

后来我们意识到:这不是代码问题,是架构问题。于是开始系统性地评估市面上的分布式事务解决方案。


主流方案横向对比:别被论文忽悠了

方案 优点 缺点 适用场景
TCC (Try-Confirm-Cancel) 强一致性,性能好 侵入性强,需写三套逻辑 支付、转账等强一致场景
Saga 易理解,支持长事务 最终一致性,回滚复杂 订单履约、审批流等
Seata AT模式 无侵入,自动回滚 性能损耗大,锁粒度粗 快速上线、中小规模系统
消息事务(RocketMQ事务消息) 解耦好,吞ut量高 依赖MQ可靠性,最终一致 日志、通知类弱一致场景

我们在内部做了POC(Proof of Concept),结论很现实:

  • TCC 虽然完美,但改造成本太高。优惠券团队直接拒绝:“我们三个前端两个后端,哪有精力写Cancel接口?”
  • Seata AT 看似香,但在高并发下(>5K TPS)数据库行锁竞争严重,压测时MySQL CPU飙到90%
  • 消息事务 在非核心链路表现不错,但履约这种“钱相关”的场景,产品经理死活不同意“最终一致”

最后我们选了 Saga + 补偿机制 + 幂等设计 的混合方案。为什么?因为业务可接受最终一致,且我们有完善的监控告警体系兜底。


实战:用Java实现一个靠谱的Saga事务

核心思想很简单:正向操作 + 异步补偿。但魔鬼在细节。

第一步:定义事务步骤(Step)

// 履约Saga的步骤定义
public enum FulfillmentStep {
    RESERVE_INVENTORY,      // 预占库存
    DEDUCT_COUPON,          // 扣减优惠券
    CREATE_DELIVERY_ORDER,  // 创建物流单
    COMPLETE;               // 完成
}

第二步:每个步骤实现补偿逻辑

@Component
public class InventorySagaStep implements SagaStep {

    @Override
    public boolean execute(OrderContext ctx) {
        // 调用库存服务预占
        return inventoryClient.reserve(ctx.getOrderId(), ctx.getSkuId(), ctx.getCount());
    }

    @Override
    public boolean compensate(OrderContext ctx) {
        // 补偿:释放预占库存
        return inventoryClient.release(ctx.getOrderId(), ctx.getSkuId(), ctx.getCount());
    }
}

第三步:事务编排器(关键!)

@Service
public class SagaOrchestrator {

    // 使用状态机驱动事务
    public void startFulfillment(OrderContext ctx) {
        ctx.setCurrentStep(FulfillmentStep.RESERVE_INVENTORY);
        
        try {
            for (FulfillmentStep step : STEP_ORDER) {
                if (!executeStep(step, ctx)) {
                    // 执行失败,立即触发补偿
                    compensateUntil(step, ctx);
                    throw new SagaExecutionException("Step failed: " + step);
                }
                ctx.setCurrentStep(nextStep(step));
            }
        } catch (Exception e) {
            log.error("Saga failed for order: {}", ctx.getOrderId(), e);
            // 异步记录失败事务,供人工干预
            sagaFailureQueue.offer(ctx);
        }
    }

    private void compensateUntil(FulfillmentStep failedStep, OrderContext ctx) {
        // 从当前步骤倒序补偿
        List<FulfillmentStep> toCompensate = getStepsBefore(failedStep);
        Collections.reverse(toCompensate);
        toCompensate.forEach(step -> compensateStep(step, ctx));
    }
}

第四步:幂等性保障(血泪教训!)

所有服务接口必须加幂等校验!我们用Redis + 唯一请求ID实现:

@PostMapping("/deduct-coupon")
public ResponseEntity<?> deductCoupon(@RequestHeader("X-Request-ID") String requestId) {
    if (redisTemplate.hasKey("idempotent:" + requestId)) {
        return ResponseEntity.ok("Already processed");
    }
    
    // 执行业务逻辑
    couponService.deduct(...);
    
    // 标记已处理(设置过期时间)
    redisTemplate.setex("idempotent:" + requestId, 3600, "1");
    return ResponseEntity.ok("Success");
}

生产环境避坑指南

  1. 补偿不是万能的
    曾经有个Bug:补偿接口被限流,导致库存一直没释放。后来我们给补偿通道单独配了高优先级线程池,并加了补偿失败告警

  2. 监控必须到位
    我们自研了一个Saga事务看板,实时展示:

    • 进行中事务数
    • 补偿触发率
    • 平均完成时长
      上周发现补偿率突增,排查发现是优惠券服务慢查询导致超时。
  3. 不要迷信“自动”
    Seata的AT模式号称“零侵入”,但在我们订单表有20+字段、频繁更新的场景下,undo_log表膨胀到TB级,DBA差点提刀来砍我。

  4. 测试要模拟网络分区
    用Chaos Mesh注入网络延迟后,才发现补偿逻辑没处理“服务完全不可达”的情况。现在CI流程强制跑混沌测试。


工具链推荐(亲测可用)

  • 事务追踪:SkyWalking + 自定义Saga插件(能看到整个事务链路)
  • 补偿重试:XXL-JOB(比Spring Retry更可控)
  • 幂等组件:自己封装的IdempotentTemplate,支持注解式使用
  • 压测工具:GoReplay回放线上流量,比JMeter更真实

最后几句大实话

分布式事务没有银弹。我在快手这几年深刻体会到:架构选择本质是成本权衡。如果你的业务允许最终一致(比如发券、积分),优先考虑消息队列;如果是资金交易,老老实实用TCC。

另外,别指望一个框架解决所有问题。Seata、Hmily这些开源项目确实降低了门槛,但生产环境的稳定性,最终还是靠完善的监控 + 人工兜底 + 团队协作。上周五晚上,我们还在群里讨论一个补偿死循环的Case,最后发现是测试同学构造的异常数据没清理干净——所以啊,工具再强,也得配上靠谱的流程。

希望这篇带血泪的实战经验能帮你少走弯路。如果你们也在搞分布式事务,欢迎留言交流(或者吐槽你们的产品经理)!

评论 0

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