分布式事务解决方案:我的五年实战心得

程序员阿远
2025-06-29 08:34
阅读 690

开篇:分布式系统带来的“幸福”烦恼

开篇:分布式系统带来的“幸福”烦恼

在做后端开发这五年里,我经历了从单体应用到微服务架构的完整演进。如果说微服务带来了架构灵活性和可扩展性,那它也带来了一个绕不开的问题:分布式事务怎么处理?

这个问题,在我参与公司核心订单系统的重构时被推到了台前。原本跑得挺好的单体系统,随着业务量的增长逐渐显现出瓶颈,我们决定将库存、支付、订单三个模块拆成独立服务。结果第一周就踩到了坑——用户下单扣了钱,但库存没扣住,最后只能手动回滚数据。

这篇文章就来说说我在实际工作中对分布式事务的一些思考与实践,希望能帮大家少走弯路。


问题描述:一次生产事故引发的血案

问题描述:一次生产事故引发的血案

事情是这样的:

我们上线了新的微服务架构,新功能支持优惠券叠加使用、分段支付等功能。但在某个大促活动上线不久后,客户投诉自己钱包余额减少了,订单状态却一直是“待支付”。更糟的是,后台日志显示支付服务调用了,但库存没扣减成功。

当时我们还没引入任何专门的分布式事务框架,仅靠本地事务 + 接口回调的方式来处理一致性问题。这种方案在正常流程下还能应付,一旦遇到异常情况(如网络超时、接口失败、重试冲突等),数据就很容易处于中间状态。

这其实就是典型的分布式事务问题:多个服务之间需要保证ACID特性,但由于没有统一的事务协调者,最终导致数据不一致。


解决方案:不是只有TCC!

说到分布式事务,很多人的第一反应就是:TCC!Seata!Saga模式!没错,这些确实是主流方案,但从我五年的经验来看,并不是所有的场景都适合用这些重型武器。

我总结出了几种常用的分布式事务处理方式,并结合不同项目背景选择合适的方案:

1. 最简单的办法——最终一致性(Eventual Consistency)

适用于高并发读写多、但对强一致性要求不高的场景。比如商品浏览记录、推荐行为统计等。

做法就是在发生变更后通过MQ异步通知其他服务完成更新。虽然有可能短暂不一致,但大多数情况下可以接受。

// 发送消息示例
rocketMQTemplate.convertAndSend("ORDER_PAY_SUCCESS_TOPIC", orderDTO);

监听方消费消息后,更新自己的状态即可。

这种方式简单粗暴、性能好,缺点是没有实时一致性保障,必须做好消费重试机制和幂等校验。

2. 基于 TCC 的补偿型事务

这是我们后来为订单系统做的正式方案,因为涉及到资金操作,必须确保数据强一致。

我们采用了阿里巴巴的开源分布式事务框架 Seata,并通过 TCC 模式来实现跨服务的一致性控制。

具体思路如下:

  • 用户下单 → 订单服务创建预订单
  • 调用库存服务进行冻结库存(Try阶段)
  • 调用支付服务尝试锁定账户余额(Try阶段)
  • 所有 Try 成功 → 提交订单并进入 Confirm 阶段,释放资源
  • 若任一 Try 失败,则执行 Cancel 操作回滚

伪代码逻辑示意:

try {
    inventoryService.freezeStock(orderDTO.getProductId(), orderDTO.getAmount());
    paymentService.lockBalance(userId, orderDTO.getTotalPrice());

    // 写入订单表,status=pending
    orderRepository.save(pendingOrder);

} catch (Exception ex) {
    inventoryService.cancelFreezeStock(...);
    paymentService.rollbackLockedBalance(...);
}

当然,这只是个简化版本,真正落地要考虑幂等、事务日志、重试机制等多个方面。

3. Saga 模式:长流程中的事务编排

我们在做供应链管理系统时遇到了一个特殊场景:一笔采购订单要依次经过供应商确认、质检、入库等环节,每个步骤都可能失败,需要灵活回退。

这种时候我们选择了 Saga 模式,即将整个流程拆分为一系列本地事务,每一步都提供正向操作及补偿动作

例如:

  1. 创建订单 → 补偿动作:删除订单
  2. 供应商确认 → 补偿:撤销确认
  3. 入库记录 → 补偿:撤销入库

我们使用 Camunda 作为工作流引擎来驱动整个流程,不仅解决了事务一致性问题,还实现了流程可视化管理和人工审批介入。


代码实践:基于 Seata 的 TCC 示例

以下是一些关键代码片段(Spring Boot + MyBatis + Seata):

添加依赖

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

注解开启全局事务

@GlobalTransactional(name = "create-order-tx")
public void createOrder(OrderRequest request) {
    inventoryService.deduct(request.getProductId(), request.getCount());
    paymentService.charge(request.getUserId(), request.getAmount());
    
    orderRepo.create(new Order(...));
}

实现 TCC 接口(以库存为例)

负载均衡配置-2

@Component
public class InventoryTccAction {

    @Autowired
    private InventoryMapper inventoryMapper;

    @TwoPhaseBusinessAction(name = "deductInventory")
    public boolean deduct(InventoryDTO dto) {
        Integer count = inventoryMapper.selectCountById(dto.getProductId());
        if(count < dto.getCount()) throw new RuntimeException("库存不足");
        
        return inventoryMapper.updateAmount(dto.getProductId(), -dto.getCount()) > 0;
    }

    @Commit
    public boolean commit() {
        // 真实业务中此处会做一些清理操作,比如写日志
        return true;
    }

    @Rollback
    public boolean rollback() {
        // 这里执行补偿逻辑
        return true;
    }
}

注意点:

  • Try 阶段一定要是幂等的
  • Confirm/Cancel 阶段也需要是幂等的,否则重试可能导致重复操作
  • 所有远程调用都需要捕获异常并主动回滚

踩坑经验:那些年我掉过的坑

数据库设计模型-1

❌ 1. 忽略幂等设计,导致重复扣款

有一次线上环境出现消息重复消费的情况,导致同一个订单被执行了两次支付操作。后来我们才意识到所有事务操作都必须加上幂等判断。

我们后来的做法是在数据库增加一个业务唯一键字段(如business_id),并在每次操作前先查询是否存在该记录。

INSERT INTO payments (order_id, amount) VALUES (?, ?) ON DUPLICATE KEY UPDATE status='DUPLICATE';

❌ 2. 事务提交顺序不当,导致死锁

刚开始用 Seata 的时候,我们同时调用了支付服务和积分服务,两个服务都有各自的数据修改操作,且互相等待对方释放资源,导致死锁。

解决办法:

  • 明确事务资源的操作顺序,避免交叉加锁
  • 引入 Redis 缓存或队列,降低直接耦合度

❌ 3. 未合理配置重试次数,压垮下游服务

有一阵子我们开启了无限重试+大量并发请求,导致下游库存服务 CPU 直接飙满,服务不可用。

调整后策略:

  • 重试次数限制(最大 3 次)
  • 使用指数退避算法控制重试间隔时间
  • 加入熔断机制(Hystrix 或 Sentinel)

效果总结:稳定性显著提升

这套方案实施后,我们的系统稳定性有了明显改善:

  • 支付 + 库存 + 订单三者之间的数据一致性得到了保障
  • TCC 模式的引入使关键路径操作具备回滚能力
  • 生产环境运行半年以上,没有因事务问题导致的用户投诉
  • 在促销期间 QPS 上升 40%,系统仍然保持稳定

另外,配合运维的同学搭建了事务监控平台,可以实时查看每笔事务的状态流转,提升了排查效率。


经验分享:别只盯着技术,更要关注业务

如果你问我这几年最大的感悟是什么,我会说:

“最好的事务设计方案,从来不是某一个流行框架,而是你对业务的理解。”

举几个我踩过的例子:

  • 当初为了追求一致性,强行用 TCC 来管理商品评论的同步,结果发现根本不需要。
  • 有些流程本身就不应该在一个事务里完成,强行绑定只会让系统变得更脆弱。

所以我建议:

  1. 先从业务角度出发,评估是否需要强一致性
  2. 优先选用轻量级方案,能不用TCC就不上
  3. 无论哪种方案,都必须考虑幂等、重试、熔断
  4. 设计事务边界时要考虑未来扩展性和运维成本

顺便提一句,现在越来越多团队开始采用事件溯源 + CQRS 的方式来处理复杂的事务场景,我个人也在关注这类架构,也许下个项目会尝试一下。


结语:技术没有银弹,合适才是关键

写完这篇,我自己也回顾了一下这些年在分布式事务上的探索之路。有时候我们会陷入一个误区,觉得只要上了 Seata 或别的框架就可以一劳永逸地解决问题,其实不然。

真正难的不是技术本身,而是如何根据实际情况做出取舍。希望这篇文章能给你一些启发,少走一点弯路,少改几次需求。

如果你也有类似的实践经验,欢迎留言交流,我们一起探讨分布式事务的最佳落地之道。


本文基于我在某电商公司的实际项目经历撰写,如有雷同,纯属巧合 🐱

评论 0

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