分布式事务解决方案:一场真实战斗的总结

清新的学者
2025-06-23 16:45
阅读 576

背景介绍:为什么我们要面对分布式事务?

背景介绍:为什么我们要面对分布式事务?

我至今还记得那个让我“彻夜未眠”的项目。那是一个典型的金融类系统,核心功能是用户充值和账户余额变更。表面上看起来很常规,但问题出在我们用了两个服务——一个负责订单管理,一个负责账户余额管理。

起初只是简单的调用链路:用户提交充值请求 → 创建充值订单 → 调用余额服务扣款。但事情并没有那么简单,随着交易量的增长,我们发现有时候订单创建成功了,但扣款失败了;或者反过来,余额扣了但订单没建起来。这时候我们就意识到:我们遇到了典型的分布式事务问题

更糟的是,系统当时没有任何补偿机制。一旦失败,整个流程就会留下脏数据,甚至导致账务对不上。这个问题直接动摇了系统的可靠性,领导层开始关注,我们也被“赶鸭子上架”,必须解决它。


问题描述:到底什么是分布式事务?

问题描述:到底什么是分布式事务?

简单来说,当我们多个服务(微服务)需要在一个业务逻辑中一起做状态变更,并且要求它们要么全部成功、要么全部失败的时候,就需要处理分布式事务。

比如:

  • 订单服务新增一条订单
  • 库存服务减库存
  • 支付服务扣钱

这三个操作都必须一致,如果其中一个失败,整个流程都要回滚。而传统的本地事务只能保证单数据库的操作一致性,跨服务时就不行了。

我们的具体场景

在我们的充值流程中:

  1. 用户发起充值 → 创建订单(order-service)
  2. 扣款 → 修改账户余额(account-service)
  3. 充值完成后发送通知(通知服务)

我们面临的核心问题是:如何保证创建订单与扣款这两个操作的原子性?也就是说,不能出现订单创建成功但未扣款,或者扣款完成但订单没生成的情况。

最开始尝试过“两步走”方式:

// step1: 创建订单
orderService.createOrder();
// step2: 扣款
accountService.charge();

结果可想而知,任何一个步骤出错,整个系统就进入了一个不一致的状态。而且由于网络不稳定、服务宕机等因素,根本无法预判错误发生在哪里。

于是我们开启了这场“对抗分布式事务”的战役。


解决方案:我们是如何搞定这个问题的?

解决方案:我们是如何搞定这个问题的?

在整个过程中,我们调研了很多种方案,包括 TCC、Saga 模式、消息队列最终一致、Seata 等。最终选择了一个混合方案,结合 TCC 和定时任务补偿机制,效果非常好。

先简单介绍下我们选中的几种策略:

1. TCC(Try-Confirm-Cancel)

这是业界主流的一种分布式事务实现方式,分为三个阶段:

  • Try 阶段:资源检查 & 预留(冻结资金)
  • Confirm 阶段:执行业务逻辑(正式扣除)
  • Cancel 阶段:回滚操作(解冻资金)

这个模式的优点是强一致性,缺点是开发成本高,每个服务都需要实现 Try、Confirm、Cancel 接口。

我们最初尝试用 TCC 的框架(如阿里开源的 Hmily),但在实际使用中发现了几个问题:

  • TCC 对代码侵入性强,改造成本大
  • 依赖本地事务日志表管理事务上下文,维护麻烦
  • 在高并发环境下,会出现重复请求、幂等性等问题

虽然可行,但我们决定暂时不用这种方式,因为项目时间紧、上线压力大。

2. 消息队列 + 最终一致性

我们采用了第二种思路:通过 RabbitMQ 实现异步消息补偿机制,这也是我们最后选择落地的方式。

基本流程如下:

  1. 用户发起充值请求
  2. order-service 创建订单(status=processing)
  3. 发送一条“待支付”消息到 MQ
  4. account-service 消费消息后进行扣款
  5. 成功后发送“已支付”消息更新订单状态为 success
  6. 如果某一步失败,定期任务检查未完成订单,重新触发补偿

这样做的优点是:

  • 架构轻量化,不依赖重装的分布式事务中间件
  • 容错能力强,即使部分环节失败也能自动恢复
  • 易于扩展,后续可以接入更多服务

当然也有缺点:

  • 不是强一致性,而是最终一致性
  • 需要自己实现幂等、去重、重试、补偿机制
  • 整体链路变长,排查问题可能较复杂

不过对于当时的业务需求而言,这是一个折中但高效的方案。


我们的实践:基于 MQ + 补偿机制的分布式事务实现

整体架构设计

[前端] -> [网关] -> [order-service]
                        ↓
                    [RabbitMQ](charge.created)
                        ↓
                 [account-service]
                        ↓
                   [update.order.status]

数据库设计要点

我们在 orders 表中增加了以下几个关键字段:

  • status:枚举类型(created, processing, paid, failed)
  • retry_count:失败重试次数
  • locked_at:订单锁定时间戳(用于超时清理)

同时增加了一张 charge_logs 表记录所有支付动作,便于审计和追踪。

核心代码示例

1. 创建订单并发送消息

@Transactional
public Order createOrder(ChargeRequest request) {
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setStatus("created");
    order.setAmount(request.getAmount());

    orderRepository.save(order);

    // 发送消息到MQ
    Message message = new Message();
    message.setType("charge.created");
    message.setContent(new ObjectMapper().writeValueAsString(order));
    rabbitMQTemplate.convertAndSend("charge.queue", message);

    return order;
}

2. 消费消息并进行扣款

@RabbitListener(queues = "charge.queue")
public void handleChargeMessage(String message) {
    Order order = objectMapper.readValue(message, Order.class);
    
    try {
        // 调用Account Service API 扣款
        boolean result = accountService.charge(order.getUserId(), order.getAmount());
        
        if (result) {
            // 扣款成功,更新订单状态为paid
            order.setStatus("paid");
            orderRepository.save(order);
            
            // 再发个通知消息
            notifyService.notify(order.getUserId(), "充值成功!");
        } else {
            // 失败更新为failed
            order.setStatus("failed");
            orderRepository.save(order);
        }
    } catch (Exception e) {
        // 日志记录 + 更新为failed
        log.error("支付失败: {}", e.getMessage());
        order.setStatus("failed");
        orderRepository.save(order);
    }
}

数据库设计模型-1

3. 定时补偿任务

我们写了一个每天凌晨运行的定时任务,用来查找状态为 failed 或 created 时间超过一定阈值的订单,重新触发扣款流程。

@Scheduled(cron = "0 0 1 * * ?")  // 每天凌晨执行
public void retryFailedOrders() {
    List<Order> orders = orderRepository.findByStatusIn(Arrays.asList("created", "failed"));

    for (Order order : orders) {
        boolean result = accountService.charge(order.getUserId(), order.getAmount());
        if (result) {
            order.setStatus("paid");
            log.info("订单 {} 已补扣款", order.getId());
        } else {
            int retryCount = order.getRetryCount() + 1;
            order.setRetryCount(retryCount);
            if (retryCount > MAX_RETRY_TIMES) {
                order.setStatus("failed");
            }
        }
        orderRepository.save(order);
    }
}

踩坑经验分享:踩过的坑比写的代码还多

在这个过程中,我们踩了不少坑,下面列出几个特别典型的问题和我们是怎么解决的。

1. 消息重复消费

问题现象:有时候同一个订单会被多次处理,导致重复扣款。

原因分析:RabbitMQ 默认是“最多一次”投递,但我们没有做好消费端幂等控制,导致重复消费。

解决方法

  • 使用订单ID作为唯一幂等Key,每次处理前查询是否已经执行过
  • 增加 Redis 缓存记录消费历史(key: charge_{orderId})
if (redisTemplate.hasKey("charge_" + order.getId())) {
    log.warn("该订单已处理,跳过...");
    return;
}
redisTemplate.opsForValue().set("charge_" + order.getId(), "processed", 1, TimeUnit.DAYS);

2. 死信队列未处理导致消息丢失

问题现象:MQ 中的消息积压了几天,没人知道为什么。

根本原因:某些消息在反复失败后变成死信,但没有设置 DLQ(Dead Letter Queue)处理机制。

解决方法

  • 配置 RabbitMQ 的死信队列插件
  • 将失败的消息自动转移到另一个队列
  • 单独监控这个队列,进行人工干预或自动补偿

3. 事务边界不清

一开始我们把 order 的保存和消息发送放在同一个事务里,结果导致事务提交前就发消息,消息内容读不到刚插入的数据。

解决方法:分离消息发送为一个单独事务,确保数据落库后再发消息。


效果总结:这套方案带来了哪些好处?

自从上线这套机制后,我们系统的稳定性有了显著提升:

维度 上线前 上线后
异常订单数 每天上百条 几乎为零
手工对账频率 每天都要查账 周级别检查即可
平均修复时长 小时级 秒级自动修复
用户投诉率 明显上升 明显下降
架构灵活性 难以横向扩展 可快速接入新服务

更重要的是,我们建立了一套完整的补偿和监控机制,为后续类似系统打下了良好的基础。


给读者的经验建议:少走弯路,从别人的坑里爬出来

如果你也在面对分布式事务的困扰,这里是我根据实战总结的一些经验建议:

✅ 1. 别一上来就追求“强一致性”

很多场景其实是可以容忍短暂的不一致性的。比如电商系统下单后1秒才显示支付成功,用户不会介意。但如果一味追求强一致性,反而会让系统变得复杂难维护。

✅ 2. 消息队列是最好的搭档

无论是 Kafka 还是 RabbitMQ,只要搭配好幂等机制和补偿逻辑,完全可以用相对简单的手段做到高可靠、低延迟的数据一致性。

✅ 3. 做好补偿机制的设计

无论你用哪种方案,必须有兜底机制。比如定时任务扫描 + 自动修复,这能让你睡得安稳些。

✅ 4. 日志+监控是救命稻草

记录每一步的状态变化,结合 ELK 做检索,出现问题能迅速定位。同时配合 Prometheus + Grafana 做可视化监控。

✅ 5. 分布式事务不要只靠中间件,更要靠设计

Seata、LCN、Hmily 等方案都有其适用场景,但它们不是万能钥匙。有时候更合适的方案,是结合业务场景做“最小化”处理。


结语:技术这条路,走得慢没关系,关键是稳

这次项目经历让我深刻理解了一个道理:技术没有银弹,只有合适与否的选择。分布式事务不是一个简单的问题,但它也不是不可逾越的大山。

从最初的焦虑不安,到后来的从容应对,这段经历不仅让我成长,也让我明白了系统设计背后的哲学:稳定高于一切,简洁胜过复杂

希望这篇文章能帮到你,哪怕只是避开了一个小坑,也是一种缘分吧 😊

如果你正在处理类似的问题,欢迎留言交流,我也很乐意听听你们的实战故事。

评论 0

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