分布式事务解决方案:从踩坑到落地的实战分享

程序员的日常信号
2025-06-15 00:00
阅读 451

背景介绍:为什么我非得碰分布式事务这块硬骨头?

背景介绍:为什么我非得碰分布式事务这块硬骨头?

去年年底,我在一家做供应链系统的公司负责一个核心模块的重构。项目背景是这样的:我们原有的系统是单体架构,所有业务逻辑都跑在一个MySQL数据库上,用本地事务就能搞定订单、库存、支付等操作的一致性。但随着业务量的增长,以及微服务化的推进,系统被拆成了多个独立服务,分别部署在不同的应用节点和数据库实例上。

这时候问题来了——用户下一单,要扣库存、生成订单、冻结资金,这三项操作分布在三个服务中,如何保证要么一起成功,要么一起失败?

没错,这就是典型的 分布式事务 场景。

刚开始没经验,直接上了两阶段提交(2PC),结果上线一测,性能崩得连开发环境都撑不住。后来又试了TCC、Saga模式、最终一致性方案……一路踩坑下来,也积累了不少血泪教训。今天就结合真实项目经验,聊聊我是怎么一步一步解决这个问题的。


问题描述:分布式环境下一致性崩溃的真实场景

问题描述:分布式环境下一致性崩溃的真实场景

我们的核心业务流程大致如下:

  1. 用户下单
  2. 订单服务创建订单记录
  3. 库存服务减库存
  4. 支付服务冻结用户资金
  5. 所有步骤成功后,订单状态变为“已支付”

理想情况下,四个步骤应该原子性地完成。但在实际运行中,经常出现以下几种异常情况:

  • 库存扣减成功,但支付失败,导致库存锁定但没人买单
  • 订单创建后,库存接口超时,前端提示用户“处理中”,但用户查不到订单
  • 幂等校验缺失导致重复处理,引发数据混乱

这些错误一旦发生,就会导致用户投诉、财务对账困难、客服压力剧增。最严重的一次,我们一个促销活动中库存出现了负数,技术组被拉着开了三轮复盘会……

所以,我们迫切需要一种既能保障数据一致性,又能兼顾性能和扩展性的分布式事务解决方案。


解决方案:选型与实践之路

解决方案:选型与实践之路

初期尝试:2PC 的高成本之痛

我们一开始选择了基于Atomikos实现的JTA分布式事务,想着用XA协议来统一协调各资源参与者。听起来很美,但实际上带来几个致命的问题:

  • 性能差:因为要两轮通信+阻塞等待,TPS跌了一半不止
  • 数据库兼容性差:不是所有的数据库都支持XA(比如某些版本的MySQL)
  • 容错能力弱:某个资源挂掉,整个事务链路都会阻塞

简单压测一下,我们就意识到这种强一致性的代价太高了。尤其是对于我们这种每天几万订单的系统来说,2PC根本扛不住流量。

于是我们开始转向其他方案。


最终采用:TCC + 异步消息补偿机制

在权衡各种方案之后,我们选择了 TCC(Try-Confirm-Cancel)模式 作为主框架,配合 RabbitMQ 或 Kafka 来做异步事件驱动。整体思路如下:

✅ TCC 实现流程:

  1. Try 阶段:资源预留

    • 订单服务创建待定订单
    • 库存服务冻结指定库存
    • 支付服务预授权金额
  2. Confirm 阶段:执行业务动作

    • 确认库存扣除
    • 确认订单生效
    • 确认资金正式划转
  3. Cancel 阶段:回滚

    • 如果任意一步失败,则触发Cancel操作释放资源

🔄 异步补偿机制:

通过消息队列将各个阶段的操作结果发布出去,并由消费端监听,进行后续确认或补偿。例如:

  • 当支付确认失败时,库存服务监听到该消息后自动解锁库存
  • 对于未完成的TCC事务,通过定时任务扫描数据库中的“待确认”状态,尝试重新 Confirm 或 Cancel

🔒 补充设计点:

  • 每个服务都要提供幂等接口(比如根据唯一订单ID判断是否已经执行过Confirm/Cancle)
  • 使用Redis缓存事务ID避免重复处理
  • 日志追踪必须完整,以便排查失败原因

关键代码片段:TCC 核心逻辑示例

关键代码片段:TCC 核心逻辑示例

以下是我们订单服务在 Try 阶段的核心伪代码:

public class OrderService {

    @Transactional
    public String tryOrder(OrderDTO orderDTO) {
        // 1. 创建订单,状态为"待确认"
        Order order = new Order();
        order.setStatus("PENDING");
        order.setUserId(orderDTO.getUserId());
        orderRepository.save(order);


![缓存策略对比-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061500/64cdbd5b-7107-4483-adf4-f490c210da2c.jpg)


        // 2. 发送 Try 成功事件
        messageProducer.send("ORDER_TRY_SUCCESS", order.getId());

        return order.getId(); // 返回事务ID用于后续确认/取消
    }

    @Transactional
    public void confirmOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        if (order.getStatus().equals("PENDING")) {
            order.setStatus("CONFIRMED");
            orderRepository.update(order);

            messageProducer.send("ORDER_CONFIRMED", orderId);
        }
    }

    @Transactional
    public void cancelOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        if (order.getStatus().equals("PENDING")) {
            order.setStatus("CANCELLED");
            orderRepository.update(order);

            messageProducer.send("ORDER_CANCELLED", orderId);
        }
    }
}

而库存服务则监听 ORDER_TRY_SUCCESS 事件并执行对应的 Try 操作。

类似地,每个服务都需要具备 Try/Confirm/Cancel 三种行为,并且要支持幂等和重试。


踩坑经验分享:那些深夜调试教会我的事

坑点一:没有幂等处理,同一笔订单被多次Confirm

我们最初没有在Confirm方法里加幂等判断,结果因网络延迟导致消息重复消费,同一个订单被反复Confirm,资金被重复扣款。

解决方案:

  • 在Confirm方法开头检查是否已经处理过当前事务ID
  • 使用Redis存储执行记录,设置合理TTL防止堆积
if (redis.exists("confirmed_order:" + orderId)) {
    log.warn("订单 {} 已确认,跳过重复处理", orderId);
    return;
}
redis.setex("confirmed_order:" + orderId, 24 * 3600, "1");

坑点二:Cancel逻辑不完善,导致死锁资源无法释放

在一次压测中,发现大量库存处于“冻结”状态,但是并没有对应订单生成。原因是Cancel方法由于数据库连接池耗尽未能及时执行。

解决方案:

  • Cancel操作不能依赖远程调用,要尽量使用本地化手段快速释放资源
  • 加入定时任务定期清理“卡住”的事务

坑点三:日志信息不全,定位问题像盲人摸象

早期日志只记录事务ID,没有上下文关联,出了问题只能靠人工翻表查找。

改进措施:

  • 统一埋点跟踪ID,贯穿整个事务生命周期
  • 接口参数、结果返回值全记录
  • 使用ELK集中日志平台 + Kibana 查询分析

效果总结:系统稳定性提升,运维复杂度可控

经过三个月的打磨和灰度上线测试,我们这套基于TCC + 异步消息补偿机制的分布式事务方案表现非常稳定:

指标 上线前 上线后
事务成功率 87% 99.2%
数据不一致率 0.5% < 0.01%
单笔交易平均耗时 320ms 180ms
运维报警次数 每天 5~6 次 基本归零

尤其在大促活动期间,整个系统面对高峰流量表现出色,没有再出现之前那种“库存负数”、“订单丢失”的问题。


给你的建议:如何优雅应对分布式事务难题?

如果你也在面临分布式事务的挑战,这里是我总结的一些经验和建议,希望能帮你少走些弯路:

1. 优先考虑最终一致性方案

除非你真的对数据一致性要求极高(比如银行转账),否则不要上来就搞强一致的2PC或3PC。这类方案性能差、复杂度高,很容易拖垮系统。

2. TCC 是相对成熟的折中选择

它虽然开发量大,但可以很好地控制事务边界和失败兜底逻辑。适合交易类场景。

3. 幂等性和重试机制要前置规划

接口设计初期就必须把幂等问题考虑进去,而不是等到上线后再补救。否则你会像我一样,在凌晨三点debug一条重复的消息消费。

4. 异步消息机制 + 可视化监控是关键

借助消息队列做解耦,加上完善的日志追踪和后台看板,可以大大降低运维成本。

5. 不要忽略人为兜底策略

即使系统设计得再严密,也难免会有一些极端情况漏网。建议保留手动回滚工具或者自动化巡检脚本,关键时刻能救命。


结语:分布式事务没有银弹,只有合适的选择

写到这里,我已经不记得和TCC打了多少场“持久战”。有时候也会想,要是当初一开始就不用微服务多好,但现实往往不允许我们这样逃避问题。

其实,分布式事务从来不是一个单纯的技术问题,它更像是一道综合题:你需要懂得数据库原理、掌握异步编程思想、理解业务规则、还要有良好的容错意识。更重要的是,你要有足够的耐心去面对它的复杂性。

希望这篇文章能给你一些启发,也欢迎你在评论区交流你们团队在处理分布式事务方面的实践经验。

毕竟,一个人走得快,一群人走得远。我们一起慢慢变强吧!

评论 0

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