分布式事务的那些坑,我是怎么踩过来的

技术拾荒者
2025-06-30 01:48
阅读 577

我是在一家中型电商公司做后端开发的,前年我们团队在做一个“秒杀+订单拆单”的重构项目时,第一次真正面对了分布式事务这一块硬骨头。以前虽然也看过一些理论知识,比如 CAP 定理、XA 协议、TCC 模型、Saga 模式等等,但真正到了生产环境里去落地,才发现课本和现实之间的差距有多大。

这篇文章就想分享一下当时那个项目的过程:为什么我们需要分布式事务?遇到了哪些坑?是怎么解决的?以及后来的一些运维经验。希望你能从中获得一些启发,少走点弯路。


项目背景

项目背景

我们的系统最初是基于一个单体架构搭建的,随着业务增长和用户体量上升,慢慢地我们将库存、订单、支付这些模块都进行了微服务拆分,逐渐形成了以下结构:

┌────────┐       ┌─────────┐       ┌──────────┐
│ 用户中心 │<----->│ 订单服务 │<----->│ 库存服务 │
└────────┘       └─────────┘       └──────────┘
                     ↑
                     ↓
                ┌────────────┐
                │ 支付服务   │
                └────────────┘

这次项目的重点是优化下单流程,支持跨区域多仓发货(也就是订单需要按商品拆单发往不同仓库),而库存服务也是按照仓库来独立部署的。

问题来了——在一个订单创建过程中,如果涉及多个仓库的商品库存减少,且其中一个库存扣减失败,整个下单应该回滚,否则就会造成超卖或者库存不一致的问题。

这时候我们就必须考虑如何在多个微服务之间保证一致性。于是,分布式事务正式摆在了面前。


面对的挑战

面对的挑战

刚开始我们尝试用数据库层面的 XA 两阶段提交,结果发现性能奇差无比,尤其在秒杀场景下几乎不可用。接着又试了消息队列配合本地事务表的方式,但在异常处理和补偿机制上特别容易出错。

具体来说,我们面临几个关键挑战:

  1. 网络波动带来的不确定性:一次调用可能超时也可能部分成功,必须设计重试机制。
  2. 数据最终一致性:不能完全依赖强一致性,需要引入异步补偿机制。
  3. 幂等性与重复操作问题:由于重试机制的存在,同一个操作可能会被多次执行。
  4. 日志追踪困难:一次下单涉及多个服务,出了问题根本不知道哪一环挂掉了。
  5. 性能瓶颈:高并发场景下,任何中间环节的阻塞都会影响整体吞吐量。

我们选择的方案:TCC + Saga 的混合模式

综合各种因素,我们最后采用了 TCC(Try-Confirm-Cancel)模型作为主方案,同时结合 Saga 模式处理一些不适合 TCC 的场景。

TCC 模型简介(以库存为例)

  • Try 阶段:预冻结库存,检查库存是否足够,预留资源但不变更实际库存;
  • Confirm 阶段:确认扣除库存,释放资源;
  • Cancel 阶段:取消操作,释放预冻结资源。

示例代码逻辑(伪代码):

// Try 阶段
public boolean tryReduceStock(Long productId, int quantity) {
    // 检查是否有足够的可用库存
    if (stockRepo.available(productId) < quantity) {
        return false;
    }
    
    // 冻结部分库存
    stockRepo.freeze(productId, quantity);
    return true;
}

// Confirm 阶段
public void confirmReduceStock(Long productId, int quantity) {
    // 扣除已冻结库存
    stockRepo.deductFreeze(productId, quantity);
}

// Cancel 阶段
public void cancelReduceStock(Long productId, int quantity) {
    // 释放冻结库存
    stockRepo.releaseFreeze(productId, quantity);
}

通过这种方式,我们在服务之间实现了比较可靠的事务控制机制。

Saga 模式辅助处理非资源类事务

对于某些不涉及核心资源的操作,比如订单状态变更记录、发送短信或通知,我们采用了 Saga 模式进行异步补偿。

比如下单完成后向用户发送一条通知短信,若发送失败,则不会直接抛异常阻止整个流程,而是记录待发送任务,在后台定时补偿执行。


技术实现细节

为了支撑整套 TCC 流程,我们做了几层基础建设:

1. 事务协调器(Transaction Coordinator)

我们并没有自己从头写一个复杂的协调器,而是借助了一个内部轻量级框架 tcc-core,这个框架主要负责:

  • 维护事务生命周期(开始、提交、回滚)
  • 管理参与服务的注册与回调
  • 日志追踪与异常捕获

大致调用过程如下:

OrderService 创建事务 --> 调用各子服务Try方法 --> Try全成功提交事务
                                               ↓
                                         否则触发Cancel

2. 异常处理和补偿机制

针对各种失败场景,我们设定了不同的补偿策略:

  • Try阶段失败:直接回滚所有已经完成的 Try 操作
  • Confirm阶段失败:自动标记事务失败,交由定时任务处理
  • Cancel阶段失败:异步补偿,避免卡住主线程

我们还开发了一个简单的补偿调度器,周期性拉取失败事务并尝试重试。

3. 幂等性保障

每个 Try / Confirm / Cancel 请求都带上唯一 transaction ID 和 operation ID,并在各服务中加缓存判断是否已经执行过。

例如:

if (redis.exists("op:" + opId)) {
    log.warn("操作已执行,跳过");
    return;
}
redis.setex("op:" + opId, 86400, "done");

这样防止因为网络重传等原因导致重复执行。


开发中踩到的坑

坑一:Cancel 回调没有及时执行,导致库存冻结未释放

我们曾经遇到一个问题:某个库存服务因为网络抖动未能接收到 Cancel 消息,结果大量库存被冻结无法使用。

解决方案

  • 引入一个库存冻结时间戳字段
  • 添加定时任务每天凌晨清空所有超过 2 小时未处理的冻结项
-- 增加冻结时间字段
ALTER TABLE inventory_frozen ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

-- 清理脚本(伪SQL)
DELETE FROM inventory_frozen WHERE created_at < NOW() - INTERVAL '2 HOURS';

这招虽然粗糙,但在当时救急非常有效。

坑二:Cancel 方法本身失败导致死循环

有时候 Cancel 自身也会报错(如数据库连接异常、锁等待超时等),我们最初的处理方式是不断重试,结果造成了内存爆掉。

改进方案

  • 增加重试次数限制
  • 对失败的 Cancel 存储到 DB 中供后续人工干预
  • 使用 Redis 记录重试次数,防止无限重试

坑三:跨地域网络延迟引发超时连锁反应

我们部署的服务在不同城市机房,有一次北京调广州的服务时,出现了大面积 TCC Try 超时。

对策

  • 调低远程调用超时阈值,快速失败而不是长时间阻塞
  • 接口增加熔断机制(Hystrix)
  • 异地部署改为同城双活 + 跨城备份

上线后的效果与收益

上线后,我们观察了两周的数据:

指标 上线前 上线后
下单成功率 91.3% 98.7%
超卖率 0.42% 0.03%
日均处理订单 12w 15w
平均响应时间 230ms 180ms

最重要的是,我们终于可以放心应对大促期间的订单流量冲击了。

而且这套机制也为后期的退款、售后、多商户入驻等功能提供了良好的扩展基础。


一些经验和建议

如果你也在处理类似的分布式事务问题,这里是我总结的一些小建议:

1. 不要一开始就把目标定为“完美一致性”

在很多业务场景中,“最终一致性”才是更现实的选择。除非像金融转账这种绝对要求,否则没必要追求强一致性。

2. 强烈推荐加入事务日志

每一笔事务的执行过程都必须记录下来,包括参与服务、状态变化、时间戳,这对排查问题是救命稻草。

你可以简单理解为给你的事务加个“行车记录仪”。

3. 接口设计要面向失败而设计

不要想着所有请求都能成功,而要想着失败的时候怎么办。接口要具备幂等性、可重试性、错误码区分明确。

4. 监控 + 告警 = 心安

我们后来在 Grafana 上接入了事务状态变化的监控指标,设置关键节点告警,一旦出现大量 Cancel 或 Confirm 失败能第一时间感知。

5. 不要轻易自造轮子

很多人一上来就想自己写事务框架,其实现在开源生态已经很成熟了,比如 Seata、Atomikos、DTX 等,都可以参考甚至直接集成。当然前提是你得了解它们的原理。


结语

分布式事务不是银弹,也不是无解之题。它本质上是对业务复杂性和系统可靠性的妥协与平衡。

在这次项目中,我们走了不少弯路,但也积累了很多宝贵的经验。最重要的是,我深刻理解了一句话:

“在工程实践中,没有最好的方案,只有最合适的方案。”

希望你也能找到属于你们系统的那条路。

如果你有类似的经验,欢迎一起交流~

评论 0

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