分布式事务解决方案:我的一次真实项目实践

睿智的太阳
2025-06-27 03:09
阅读 752

引言:为什么我们需要关注分布式事务?

引言:为什么我们需要关注分布式事务?

在我做后端开发的这几年里,最让我印象深刻的一个问题,就是“分布式事务”这个老大难问题。尤其是在当前微服务盛行的时代,一个业务流程往往需要多个服务协同完成。每个服务有自己独立的数据库,看似清晰解耦,但一旦涉及到跨服务的数据一致性,各种数据不一致、事务失败的问题就层出不穷了。

今天我想通过一次真实项目的经历,来分享我们在分布式事务方面的解决方案和实践经验。文章会结合背景介绍、遇到的具体挑战、解决思路、实现代码、踩坑经验以及最终效果等几个方面展开,内容尽量做到深入浅出,适合刚接触微服务架构的同学理解掌握。

项目背景:从传统单体系统到微服务转型

项目背景:从传统单体系统到微服务转型

我们是一家做电商的公司,系统最初是一个典型的单体架构,所有模块(用户、订单、库存、支付)都在一个项目中运行。随着业务发展和访问量增长,这种结构的弊端逐渐显现:

  • 各个功能模块耦合严重,升级部署困难
  • 单点故障影响整个系统的可用性
  • 数据库连接瓶颈明显,尤其在大促期间经常出现锁等待和超时

于是我们启动了微服务化重构计划,将原本的单体应用拆分成了多个独立服务,比如:

  • 用户中心:负责用户的注册、登录、权限管理
  • 订单中心:下单、取消、查询订单状态
  • 库存中心:商品库存管理与扣减
  • 支付中心:处理交易流水和支付回调

拆得是干净利落了,但在实际落地过程中,一个新的问题浮出水面:如何保证跨服务的数据一致性?

特别是订单创建过程,涉及多个服务之间的协调操作:

  1. 用户下单请求进入订单中心
  2. 订单中心要调用库存服务确认商品是否有货
  3. 扣除库存后,生成订单并保存
  4. 订单创建成功后触发支付,由支付中心处理交易流程

如果这中间任何一个环节失败,都会导致数据不一致,比如已经扣减库存但订单没生成,或者订单生成了但支付失败等等。

这就引出了我们要讨论的核心问题——分布式事务的一致性保障


面临的挑战:真实的场景问题

面临的挑战:真实的场景问题

挑战一:强一致性 vs 最终一致性

在设计之初,团队对是否要保证强一致性存在分歧:

  • 坚持强一致的人认为,必须保证“要么全部成功,要么全部回滚”,否则数据不一致会带来后续难以追责的问题
  • 主张最终一致性的则认为,在高并发、多节点的场景下,追求强一致性可能牺牲性能,而且实现起来复杂度太高

这个问题最后还是从业务角度去权衡,得出结论:订单创建这类核心交易操作必须保证强一致性,其他如用户行为统计、优惠券发放等可以接受一定的延迟一致性。

挑战二:技术选型难题

我们先后评估了几种常见的分布式事务解决方案:

  1. 两阶段提交(2PC)

    • 实现简单,但存在单点故障风险,性能差,不适合高频写入场景
  2. TCC(Try-Confirm-Cancel)模式

    • 灵活可扩展,但开发工作量较大,需要为每个操作编写 try、confirm、cancel 三个接口
  3. Saga 模式

    • 更加轻量级,适用于长周期流程,但补偿机制复杂且需要幂等性和重试逻辑
  4. 基于消息队列的最终一致性方案

    • 异步解耦能力强,但无法保证实时一致性,适合非关键路径的数据同步
  5. Seata、Atomikos 等框架集成

    • 封装程度高,但学习成本较高,同时对已有系统改造力度较大

经过评估,结合我们当时的技术储备、人力投入以及上线时间窗口,最终决定采用 TCC + 本地事务表 + 补偿任务调度 的方式来应对这一挑战。


解决方案:TCC + 补偿机制的组合拳

我们最终的分布式事务方案是基于 TCC 思路设计的,但为了减少实现成本,没有采用开源框架,而是选择手动实现,并辅以日志记录和后台补偿机制。

核心流程图如下:

[订单服务]        [库存服务]         [支付服务]
    |                |                 |
    |----(1) Try---->|                 |
    |<----(OK)-------|                 |
    |                |                 |
    |----(2) Try---->|                 |
    |<----(OK)-------|                 |
    |                |                 |
    |-(3) Confirm-> |                 |
    |<--(Done)-------|                 |
    |                |                 |
    |--------(4) Confirm----------->| 
    |<--------------(Done)----------|

具体实现步骤如下:

1. Try 阶段(资源预留)

订单中心接收到用户下单请求后,首先开始事务流程:

  • 调用库存服务的 decreaseStockTry() 接口,进行库存预扣减
  • 调用支付服务的 prePay() 接口,冻结相应金额
  • 如果其中任何一步失败,则直接终止流程,释放已占用资源

2. Confirm 阶段(业务执行)

当所有 Try 成功完成后:

  • 调用库存服务的 confirmDecreaseStock() 方法,正式扣减库存
  • 调用支付服务的 doPay() 接口,真正完成支付操作
  • 创建订单记录并保存

3. Cancel 阶段(异常回滚)

在任意环节发生异常或超时的情况下:

  • 调用库存服务的 cancelDecreaseStock() 方法,释放之前预扣的库存
  • 调用支付服务的 cancelPrePay() 方法,解冻账户资金
  • 可选:发送通知给相关人员进行人工干预或自动补偿

4. 日志追踪与补偿机制

我们使用了一个事务状态追踪表来记录每次分布式事务的执行情况:

CREATE TABLE distributed_transaction_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  business_id VARCHAR(64), -- 业务ID,如订单号
  service_name VARCHAR(128), -- 当前服务名
  phase VARCHAR(32), -- try/confirm/cancel
  status ENUM('PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'),
  retry_times INT DEFAULT 0,
  last_retry_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

每天凌晨跑一次定时任务,扫描所有未完成的事务日志,根据状态进行重试或补偿处理。


关键代码片段展示

下面是一些核心代码片段,展示事务是如何在各服务间流转的。

订单服务:主流程控制

public Order createOrder(String userId, String productId) {
    String orderId = generateOrderId();
    
    // Step 1: Try 扣库存
    boolean stockReserved = inventoryService.decreaseStockTry(productId, 1);
    if (!stockReserved) {
        throw new BusinessException("库存不足");
    }
    
    // Step 2: Try 冻结支付
    boolean payFrozen = paymentService.prePay(userId, totalPrice);
    if (!payFrozen) {
        inventoryService.cancelDecreaseStock(productId, 1);
        throw new BusinessException("支付失败");
    }

    // Step 3: 确认扣库存
    boolean confirmedStock = inventoryService.confirmDecreaseStock(productId, 1);
    if (!confirmedStock) {
        paymentService.cancelPrePay(userId, totalPrice);
        inventoryService.cancelDecreaseStock(productId, 1);
        log.error("库存确认失败,进入人工补偿流程");
        triggerManualCompensation(orderId);
        return null;
    }

    // Step 4: 完成支付
    boolean paid = paymentService.doPay(orderId);
    if (!paid) {
        log.info("支付失败,准备Cancel流程");
        inventoryService.cancelDecreaseStock(productId, 1);
        paymentService.cancelPrePay(userId, totalPrice);
        return null;
    }

    // Step 5: 创建订单
    Order order = saveOrder(orderId, userId, productId);
    return order;
}

库存服务:TCC 接口示例

public class InventoryServiceImpl implements InventoryService {

    @Override
    public boolean decreaseStockTry(String productId, int quantity) {
        // 在Redis中记录预扣库存
        Long currentStock = redis.get(productId);
        if (currentStock < quantity) {
            return false;
        }
        redis.incrBy(productId, -quantity);
        logTransaction(productId, "inventory", "try", "pending");
        return true;
    }

    @Override
    public boolean confirmDecreaseStock(String productId, int quantity) {
        // 正式从DB中扣除库存
        int affectedRows = inventoryDAO.decreaseStock(productId, quantity);
        if (affectedRows > 0) {
            redis.del(productId); // 删除预扣记录
            logTransaction(productId, "inventory", "confirm", "success");
            return true;
        } else {
            log.error("确认扣库存失败");
            return false;
        }
    }

    @Override
    public boolean cancelDecreaseStock(String productId, int quantity) {
        // 返还预扣库存
        redis.incrBy(productId, quantity);
        logTransaction(productId, "inventory", "cancel", "success");
        return true;
    }
}

开发中的坑和教训总结

在实际开发过程中,我们也遇到了不少问题,这些经验值得后来人注意:

坑一:幂等性处理不到位导致重复扣减

初期设计时,我们忽视了接口的幂等性处理,导致在网络抖动或重试时,同一个接口被多次调用,造成库存或支付重复扣除。

解决方法:我们在每个接口中都加入了 requestId 参数,用于标识本次请求唯一标识,并在服务端建立请求ID防重机制,使用 Redis 或 MySQL 去重表进行判断。

坑二:补偿机制不够健壮,导致死循环或遗漏

早期我们依赖异步定时任务进行补偿,但由于日志记录格式不统一、状态更新顺序错误等问题,导致部分事务永远处于 “PENDING” 状态,甚至出现无限重试、死循环。

解决方法

  • 增加最大重试次数限制,超过次数转人工介入
  • 使用分布式锁防止同一事务被多个任务同时处理
  • 统一日志记录格式并引入监控报警机制

坑三:服务之间通信失败导致阻塞主线程

在 Try 和 Confirm 阶段中,我们最初采用了同步 HTTP 请求的方式进行服务调用,结果在高并发下大量线程被阻塞,系统整体性能下降。

解决方法:引入异步+Future模式,结合线程池进行异步编排,提升吞吐能力。


实施后的效果与收益

在生产环境运行了一段时间后,我们的这套分布式事务机制取得了以下成效:

  • 订单创建成功率提升了约 12%,主要得益于良好的失败重试机制
  • 数据一致性得到了有效保障,关键路径上未再出现明显的不一致问题
  • 由于采用了异步补偿机制,高峰期的系统稳定性更高,响应更迅速

运维同学反馈说,通过日志跟踪和后台补偿任务的优化,排查问题的速度快了不少,也减轻了人工干预的压力。


我的经验建议

如果你正在考虑微服务下的事务一致性问题,以下是我总结的一些建议:

✅ 明确业务需求边界

不是所有操作都需要严格的一致性。区分“关键路径”和“非关键路径”,合理选择最终一致性还是强一致性。

✅ 技术方案要因地制宜

不要盲目追求高大上的框架,像 Seata 这样的中间件虽然功能强大,但如果你们团队缺乏相关的维护能力,反而可能成为负担。

✅ 幂等性和重试机制是必须项

无论哪种分布式事务方案,都要考虑网络不确定性带来的重复请求,做好幂等校验,避免“多扣多减”。

✅ 加入可观测能力

包括日志记录、链路追踪、监控报警,是发现问题的关键手段。可以结合 SkyWalking、Zipkin 等工具辅助排查问题。

✅ 善用本地事务表 + 异步补偿

比起复杂的事务协议,一个设计合理的本地事务表配合补偿任务,很多时候能起到事半功倍的效果。


结语:每一步探索都是价值积累

写到这里,我突然想起在项目实施过程中的一些小插曲。

还记得有一次因为 Redis 缓存预扣值和数据库库存不同步,导致一个用户连续下了三次订单才成功。那天晚上我们加班复盘,最后发现问题根源竟然是一个简单的缓存清理时机错误。

那一刻我明白了一个道理:不管多么先进的架构,底层依然是一个个具体的细节堆砌而成的。分布式事务并不神秘,它本质上是对业务流程的精细化控制和对失败场景的周全考虑。

希望这篇文章能帮你在面对类似问题时少走弯路,也希望你能在实战中不断摸索出更适合你团队的解决方案。

如果你有任何疑问或想法,欢迎留言交流~

评论 0

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