分布式事务解决方案:从踩坑到落地的最佳实践
引言:为什么我需要面对分布式事务?

在我过去五年的后端开发经历中,分布式系统逐渐成为了主流架构。从最初的单体应用逐步演进到微服务架构,再到现在的云原生、Kubernetes + Service Mesh 组合拳,业务的复杂度不断提升,而“数据一致性”这一核心问题也变得越来越棘手。
尤其在一次电商系统重构项目中,我们遇到了典型的跨服务订单创建与库存扣减场景——用户下单时,订单服务要新建订单记录,同时库存服务要减少对应商品数量。如果其中一个服务执行成功而另一个失败,就会出现超卖或者订单无效等严重问题。这正是典型的分布式事务场景。
那段时间,我们一起尝试过多种方案,有本地事务+消息队列补偿机制,也有基于Seata的强一致性事务管理器,还有最终一致性的异步处理方式……每一种方案都伴随着不同的挑战和教训。今天我想结合那次项目的实战经验,来谈谈我们在分布式事务上的最佳实践。
问题描述:一场看似简单的跨服务操作引发的事故

那次事故发生在我们上线新版本电商系统的第二天。一个用户下了大额订单,订单状态被创建成功,但库存未被正确扣除。结果过了几小时,又有另一个用户下单相同商品并成功支付,导致系统出现了明显的超卖行为。
后来排查发现:
- 订单服务创建订单用的是本地事务控制,插入数据库成功;
- 但在调用库存服务(通过Feign远程调用)时网络不稳定发生了Timeout;
- 系统当时没有重试也没有回滚机制,订单就“半成功”了。
最麻烦的是,由于两个服务是部署在不同JVM实例甚至不同机房的微服务,传统的本地事务已经失效。也就是说,我们面临了一个跨越多个独立服务的数据一致性保障问题。
这直接促使我们开始全面审视当前系统的事务管理能力,并重新设计整个下单流程的事务边界。
解决方案:如何应对分布式事务?
经过几轮讨论和技术验证,我们最终采用了混合型事务处理模式,针对不同场景采用不同策略:
- 对于高一致性要求的场景(如支付完成后的资金变动),使用阿里开源的 Seata(TCC模式)。
- 对于可以接受短时间不一致的场景(如下单、库存扣减),采用本地事务表+消息队列补偿机制(RocketMQ) 实现最终一致性。
下面我重点讲一讲这两个方案在我们项目中的具体实现和落地经验。
关键技术选型与实现思路
✅ Seata TCC 模式实现强一致性事务
场景举例:
用户完成付款操作后,系统需同步更新订单状态为“已支付”,并调用财务服务增加收入流水。这两者必须保持事务一致性,否则会影响后续对账和风控系统。
架构设计:
- 使用 Spring Cloud Alibaba + Nacos + Seata
- 设计事务协调者 TC(Transaction Coordinator)
- 每个参与事务的服务实现
Try,Confirm,Cancel接口
核心实现逻辑:
// 订单服务 - Try阶段
@TwoPhaseBusinessAction(name = "deductBalanceAndCreatePayment")
public boolean tryDeduct(BusinessActionContext ctx) {
// 冻结余额
freezeBalance(userId, amount);
return true;
}
// Confirm阶段
@Commit
public boolean confirm(BusinessActionContext ctx) {
// 生成真实支付记录
createPaymentRecord(userId, amount);
return true;
}
// Cancel阶段
@Rollback
public boolean cancel(BusinessActionContext ctx) {
// 回退冻结金额
unfreezeBalance(userId, amount);
return true;
}
Seata 的优点在于它可以很好地集成到微服务中,并且提供了全局事务ID追踪能力,适合金融类强一致性需求。
缺点也很明显:
- 接口需要多写两套代码(Confirm/Cancel),增加了开发负担;
- 如果 Cancel 方法也失败,需要依赖事务日志进行人工介入;
- 性能较弱,特别是当事务链路较长时,延迟会显著上升。
✅ 本地事务 + RocketMQ 实现最终一致性
场景举例:
下单动作包括订单创建、库存扣减、积分变更等多个步骤。我们希望快速响应用户请求,允许短暂不一致,但要在几分钟内达成一致性。
实现方式:
- 在订单服务内部创建一个事务消息表,用于记录每个事务的状态(例如“已提交”、“待确认”、“已完成”)。
- 下单事务首先在一个本地事务中创建订单记录 + 插入事务消息。
- 之后发送一条 RocketMQ 消息通知其他服务进行处理。
- 其他服务消费消息后修改库存或积分,并返回ACK。
- 消息回查机制确保所有步骤最终达成一致。
核心事务消息结构设计示例:
CREATE TABLE order_transaction (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
business_key VARCHAR(64), -- 订单ID
event_type ENUM('ORDER_CREATE', 'INVENTORY_DEDUCT'),
status ENUM('PENDING', 'COMMITTED', 'FAILED') DEFAULT 'PENDING',
created_at DATETIME,
updated_at DATETIME
);
消息生产伪代码:
@Transactional
public void createOrder(Order order) {
// 创建订单记录
orderRepository.save(order);
// 插入事务消息
transactionMessageService.create(
new TransactionMessage(order.getOrderId(), "ORDER_CREATE", "PENDING")
);
// 发送消息给库存服务
rocketMQTemplate.convertAndSend("TOPIC_INVENTORY", order.getItemId());
}
RocketMQ 支持事务消息,可以在回调中判断是否提交或回滚事务。这种方式非常适合不要求实时一致性的场景,性能也比 Seata 高出不少。
踩坑经验:那些年我们一起趟过的坑
⚠️ 坑1:事务消息丢失怎么办?
一开始我们用普通的消息队列做补偿,结果遇到消息重复投递的问题。后来改为 RocketMQ 的事务消息机制后,还需要自己实现反查接口。
解决方法:
- 所有事务消息入库,记录状态
- 实现 RocketMQ 的 CheckListener,在 Broker 定时反查时检查数据库状态
⚠️ 坑2:Cancel 方法失败没兜底
Seata 的 Cancel 方法执行失败时,默认只是记录日志,但不会自动重试。我们的一次压测中出现大量事务回滚失败,最后只能通过手动运维恢复数据。
解决方法:
- 自定义事务日志表,定时扫描异常事务
- 结合定时任务做补偿重试机制
- 增加人工干预入口
⚠️ 坑3:事务粒度过粗影响性能
最初我们将整个下单流程包裹在一个 Seata 全局事务里,QPS 下降严重,尤其是在高并发下,TC 成为瓶颈。
解决方法:
- 对于不需要强一致性的环节,拆出去使用最终一致性方案
- 将库存扣减改为异步 + 补偿方式,只保留关键数据一致性由 Seata 控制
效果总结:上线后的收益与改进方向
上线新事务模型后,我们的系统稳定性有了明显提升:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 超卖率 | 0.3% 左右 | 基本归零 |
| 下单响应时间 | 平均 800ms | 平均 200ms |
| 系统可用性 | 偶尔因事务阻塞触发限流 | 稳定在99.95%以上 |
我们也得到了运维同学的好评,因为现在可以通过事务ID快速追踪上下游调用链,出了问题也能快速定位和修复。
不过这条路还没走完,目前我们正在探索更轻量化的事务处理方式,比如:
- 使用 Dapr 提供的 SAGA 编排模式
- 基于 Event Store 和 CQRS 实现事件驱动架构
- 探索 LCN、ByteTCC 等其他中间件的可能性
经验分享:写给正在战斗的你
如果你也在开发一个多服务系统,并考虑引入分布式事务,请记住以下几点经验:
🔍 明确一致性级别
- 强一致性:适用于涉及资金、核心资源变动的场景;
- 最终一致性:适用于大部分读写分离、非核心数据更新;
🔄 选择合适的事务模式
- 不要一开始就全上 Seata,除非你真的非常需要它;
- 大多数时候,合理设计补偿机制 + 事务消息 + 人工兜底就够了;
🛡️ 数据库设计不能忽视
- 即使用了事务框架,底层数据库也要做好分库分表、读写分离、索引优化;
- 有些时候,把相关性强的表合并(比如订单+商品信息)反而能大大简化事务逻辑;
📊 监控和报警一定要跟上
- 事务日志必须可查询、可追踪;
- 定期统计事务成功率、失败重试次数,建立预警机制;
💬 多和团队沟通协作
- 各个服务之间要有统一的消息规范和状态码;
- 事务不是一个人的事,而是整个团队都要参与进来的事情;
结语:分布式事务的本质是工程权衡的艺术
回头来看,我越发觉得分布式事务并不是一个纯粹的技术问题,而是一个工程权衡的过程。它涉及到系统设计、数据建模、运维保障、甚至是产品策略。
在这个过程中,最重要的是我们要清楚自己的业务需求是什么,愿意为一致性付出多少代价,以及能否承担不一致带来的风险。有时候,放弃“绝对正确”换来的可能是更好的用户体验和更高的系统吞吐。
希望我的这段经历能对你有所启发。如果你也在实践中遇到类似问题,欢迎留言交流。让我们一起在复杂的分布式世界中找到属于自己的那份“一致性之美”。

评论 0