我对比3种分布式事务方案后,总结出的踩坑实录

项目背景与选型纠结

前阵子接了一个订单中台的重构项目,客户是一家做跨境电商的老牌电商,年订单量大概在 1200 万左右。他们现在的痛点是:下单流程太长了,库存服务、订单服务、积分服务分属不同的团队维护,数据一致性经常出问题。有一次大促期间,订单创建成功了,但库存扣减因为网络抖动没回来,结果超卖了 300 单,客户骂得很凶。

说实话,这次接手我是有点忐忑的。之前的系统是用单体架构硬撑的,现在拆成了微服务,但事务一致性还是用旧思维在处理,全是硬编码的补偿逻辑,维护起来简直是灾难。

客户希望我们重构这个链路,保证数据最终一致性,同时不能影响现有的性能。当时我们技术团队内部吵得不可开交,主要围绕三个方案争论不休:Seata 的 AT 模式、TCC 模式,以及基于消息表的本地事务方案。

我当时的第一反应是,直接用 Seata AT 吧,配置简单,业务代码几乎不用改。我觉得这样就行,结果发现错了。Seata AT 虽然方便,但它对数据库的锁持有时间较长,在大促这种高并发场景下,容易导致数据库连接池爆满,系统直接卡死。这个坑,我们后来才深刻体会到。

试了一圈发现,对于这种订单核心链路,不能只看开发效率,还得看性能和稳定性。最终我们选择了「TCC + 消息表」的混合方案。下面我详细说说这个过程。

实战中的关键决策

我们决定在库存扣减环节使用 TCC,而在订单状态同步环节使用消息表。这个决策是怎么来的呢?

当初我有方案 A 和方案 B。方案 A 是全链路使用 Seata AT,方案 B 是核心链路 TCC,非核心链路消息表。我选了 B,因为订单服务涉及资金,对一致性要求极高,TCC 可以精确控制事务范围,而积分发放这种最终一致的场景,用消息表更轻量和稳定。

在实现 TCC 的时候,我们遇到了一个很有意思的问题。TCC 要求开发者手动编写 Try、Confirm、Cancel 三个阶段。刚开始,我觉得 Try 阶段只是预占资源,Confirm 和 Cancel 只是状态变更,应该很简单。

结果在测试环境中,我们发现 Try 阶段提交成功,但 Confirm 阶段因为下游服务超时失败了。按照 TCC 的设计,Try 阶段的资源需要预留,但不能真正占用。我们在数据库中加了一个 status 字段来区分状态,但这导致了大量的更新操作。

坑死了,有一次线上测试,由于 Confirm 重试次数过多,导致数据库 CPU 飙升到 90%。我们排查了很久,才发现是重试机制没有设置退避策略,一直高强度轮询。

最终,我们优化了重试逻辑,引入了指数退避,并且在 Try 阶段使用了 Redis 缓存来减少数据库压力。下面是核心的 TCC 实现代码:

```java
@Component
public class StockTccActionImpl implements TccAction {

@Autowired
private StockService stockService;

@Override
public void tryAction(String transactionId, Object parameter) {
// 预占库存,更新状态为 PENDING
stockService.reserveStock(transactionId, (Long) parameter);
}

@Override
public void confirmAction(String transactionId, Object parameter) {
// 正式扣减库存,更新状态为 CONFIRMED
stockService.confirmStock(transactionId);
}

@Override
public void cancelAction(String transactionId, Object parameter) {
// 释放预占库存,更新状态为 CANCELLED
stockService.cancelStock(transactionId);
}
}
```

有意思的是,在消息表这块,我们没有用 RocketMQ 的事务消息,而是用了自建的本地消息表。原因是客户现有的数据库是 MySQL,使用本地消息表可以利用数据库本身的事务特性,保证消息写入和业务操作在同一事务中,更加可靠。虽然需要定时任务扫描发送,但逻辑简单,容易排查问题。

踩坑经历与反思

整个项目下来,我最大的感触是:分布式事务没有银弹。

之前我总觉得 Seata 能解决所有问题,结果在性能测试时被打脸。AT 模式的全局锁机制在高并发下确实是个瓶颈。而 TCC 虽然性能好,但开发成本太高,需要人工编写三个阶段,容易出错。

消息表方案则是折中之选,它在一致性和性能之间找到了平衡点,适合大多数非强一致性的场景。

还有一个踩坑的经历是,我们在 TCC 的 Confirm 阶段,没有做好幂等性校验。有一次网络抖动,导致 Confirm 被调用了两次,库存被扣减了两次。后来我们在数据库中加了唯一索引,并且在业务逻辑中增加了幂等性检查,才解决了这个问题。

总之,分布式事务的选型,一定要结合具体的业务场景,不能盲目追求新技术。Seata、TCC、消息表各有优劣,选对方案比用好方案更重要。

本文基于实际项目经验整理,欢迎在评论区交流技术问题。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐