分布式事务解决方案:我的实战总结和踩坑经验
开篇:一个订单系统的分布式“噩梦”

在我参与的一个电商后台项目中,我负责设计并实现一套订单系统。这个系统一开始的架构还算简单:用户下单后,调用商品服务减库存、调用用户服务扣积分、调用支付服务处理付款,最后落单。各个模块通过 Dubbo 接口进行通信,数据库也各自独立。
但随着业务复杂度增加,特别是平台引入了第三方优惠券、限时抢购、积分抵现、预售退款等一系列功能之后,我们频繁遇到了跨服务数据一致性的问题。最严重的一次是:用户下单成功,支付完成,但是由于某个服务超时导致最终订单状态混乱,既没有退掉已使用的积分,也没有回滚库存,造成了用户的投诉和财务对账的巨大压力。
这个问题的本质就是——分布式事务处理不当。
于是,我开始深入研究和实践各种分布式事务的解决方案,结合团队的实际情况,最终形成了一套适合我们当前阶段的分布式事务管理机制。
这篇文章,是我在这条路上的摸索、踩坑与收获。希望能给正在面对类似问题的你一些启发。
问题描述:数据不一致,究竟谁背锅?

让我回忆一次典型的故障场景。
场景一:下单失败后积分未退回
用户购买了一个价值100元的商品,使用了80积分抵扣20元,支付80元现金。整个流程包括:
- 下单接口调用
- 商品服务减库存(RPC)
- 用户服务扣除积分(RPC)
- 支付服务发起支付(同步)
- 订单中心落库并返回结果
但在某次促销期间,由于商品服务出现短暂宕机,整个流程中断在步骤2。按理说应该整个流程回滚,但由于各服务之间没有任何事务保障,最终出现:
- 积分被扣除了
- 库存没减少
- 订单没创建
- 支付也没有触发
用户懵圈,客服炸锅,技术团队彻夜查日志。
这就是一个典型的跨服务数据一致性缺失带来的灾难性后果。
挑战分析
在这种多服务协作的场景下,主要挑战如下:
- 服务之间强依赖?还是异步松耦合?
- 数据一致性如何保证?是强一致还是最终一致?
- 出现异常是否要人工干预?
- 系统吞吐量能否接受事务带来的性能损耗?
这些问题都需要我们在实际业务场景中做出权衡和选择。
解决方案:基于本地消息 + 最终一致性的补偿机制

考虑到我们的系统还在快速迭代中,且短期内对强一致性的要求并不极端,同时团队经验集中在 Java 生态,所以我们选择了以最终一致性为核心的轻量级分布式事务方案,具体实现如下:
方案名称:本地事务 + 异步补偿 + 最终一致性
核心思路:
- 所有涉及到多个服务变更的操作,都必须首先在一个本地事务中记录一个“事务操作日志”。
- 然后再调用其他服务完成对应操作。
- 如果中途出错,则通过定时任务或MQ监听来进行补偿回滚。
这种做法本质上是一种“伪分布式事务”,但胜在实现简单、风险可控,在我们这种中小规模系统中非常合适。
举个例子:
比如下单流程,我们会先开启一个本地事务:
@Transactional
public void placeOrder(OrderDTO order) {
// Step 1: 创建订单,插入订单表,并写入事务操作日志
orderMapper.insert(order);
// Step 2: 写入一条事务日志,表示“准备扣除积分”
logService.recordAction("deduct_points", userId, pointsUsed);
// Step 3: 调用用户服务扣除积分
userService.deductPoints(userId, pointsUsed); // RPC
// Step 4: 调用商品服务减库存
productService.decreaseStock(productId, quantity); // RPC
// Step 5: 更新订单状态为“待支付”
orderMapper.updateStatus(orderId, "pending_payment");
}
这段代码的核心在于,我们在同一个本地事务中,完成了订单落库和操作日志的记录。也就是说,如果在执行到userService.deductPoints()之前抛出异常,整个事务都会回滚;但如果是在调用RPC时发生了网络异常、超时或者服务不可用等情况,此时本地事务已经提交,我们需要靠后续的补偿机制来兜底。
补偿机制的设计
- 补偿定时任务:
我们有一个专门的定时任务扫描所有的“未完成事务”,根据日志中的操作类型去检查当前状态是否符合预期:
- 比如发现某笔订单的日志显示“已扣除积分但未支付”,则触发积分返还操作;
- 或者发现库存未减,但订单已经成立,需要重新调用减库存接口。
- 消息队列监听补偿:
我们也接入了 RocketMQ 的消息机制,当某个服务发生关键状态变化时,会发一条状态变更事件,例如“支付完成”、“订单关闭”等。
消费者端收到这些消息后,会去核对相关事务状态,并做对应的补偿动作。
技术细节补充
- 日志表结构大致如下:
CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
business_type VARCHAR(50), -- 如"order_create"
action_type VARCHAR(50), -- 如"deduct_points"
target_id VARCHAR(64), -- 操作对象ID,如用户ID
status ENUM('init','success','failed','compensated'),
create_time DATETIME,
update_time DATETIME,
retry_count INT DEFAULT 0
);
- 在微服务架构中,每个事务日志的生成方应具有唯一标识,并尽量做到幂等处理。
效果总结:稳定了不少,效率也能接受

自这套机制上线以来,我们的分布式事务问题明显减少:
- 数据一致性得到了有效保障:通过补偿逻辑,绝大多数的异常情况都能在几分钟内自动恢复;
- 减少了人为介入:以前每天都要手动修复几个异常订单,现在基本上只有极端情况下才会报警;
- 提升了系统可观测性:通过日志+监控手段,我们可以清楚地看到每个事务的状态流转,便于后期优化。
在性能方面,虽然每次下单流程比原来多了几毫秒(主要是写日志和RPC开销),但整体影响在可接受范围内。毕竟相比用户等待,我们更怕的是数据错乱。
经验分享:别想着一口吃成胖子
在整个过程中,我想特别强调几点心得:
1. 不要上来就上TCC / Seata / XA
很多人一遇到分布式事务,第一反应就是“要不要上Seata?”、“是不是得用XA协议?”、“是不是得引入Saga模式?”等等。
但我建议,先搞清楚你的业务需求到底是什么:
- 是真需要严格的ACID吗?
- 还是能容忍一定时间的数据不一致?
- 吞吐量有没有特别高的要求?
- 团队是否有能力维护复杂的分布式事务组件?
如果你的系统只是几十QPS的业务线,那可能压根不需要重武器,就像我上面讲的“本地事务+异步补偿”就能解决问题。
2. 优先考虑最终一致性,而不是强一致性
这其实是一个架构哲学问题。
现实世界的大多数业务场景,其实是可以接受短时间内不一致的,比如:
- 支付后库存还没减完,用户看不到立即的变化没问题;
- 积分扣了但订单取消了,只要能在几分钟内自动退回就行;
- 退货退款流程允许异步处理,用户也不差这十几分钟。
所以,与其追求强一致性带来的高昂代价,不如思考怎么设计一个优雅的补偿机制。
3. 补偿逻辑要幂等、要可重试、要可追踪
这是我在实践中踩过的一个坑。当初的补偿代码没有做幂等判断,导致某个用户的积分被多次返还,引发财务对账混乱。
后来我们加了两层机制:
- 幂等校验字段:每个事务操作带一个唯一ID,记录已执行过的ID;
- 重试次数限制:最多尝试三次,超过则告警;
- 监控告警体系:所有未完成事务都会上报指标,一旦积压超过阈值立刻告警。
这样就可以把补偿机制做成一个相对靠谱的“兜底”。
4. 做好监控和告警,避免“静默失败”
有时候你不知道你的补偿没跑起来,那才是最大的灾难。
我们后来接入了Prometheus + Grafana来做事务状态的可视化监控,加上钉钉/飞书告警,确保每一步都有可观测性。
技术趋势:分布式事务的未来方向
目前来看,越来越多的云厂商都在推出自己的分布式事务中间件解决方案,比如阿里云的GTS、腾讯云的DTS,还有Apache ShardingSphere和Fescar(现在的Seata前身)的发展也在推动着标准化的进程。
对于大型企业或高并发场景,使用成熟的分布式事务框架确实更省事。但对于中小型项目或创业团队来说,我仍然推荐使用我前面说的“本地事务+异步补偿”的方式,它足够轻量,足够实用,也更容易落地。
另外,随着事件驱动架构(EDA)和CQRS等架构模式的普及,很多系统逐渐从“同步请求响应”转向“异步消息驱动”,这也降低了对分布式事务的依赖。
结尾:分布式事务,不是银弹,也不是洪水猛兽

作为一个全栈开发者,我亲身经历了从原始单体架构到微服务化的全过程,也见证了分布式事务问题带来的种种困扰。
但我也明白了,分布式事务本身不是问题,真正的问题是我们是否理解自己的业务需求,以及是否能够找到一种合适的平衡点。
希望我的这一段经历,能帮助你少走一些弯路。
如果你也有类似的经历,或者在实际开发中遇到类似的难题,欢迎留言交流。我们一起踩坑,一起进步。
✨ 文章作者:一位热爱编程、喜欢折腾的后端工程师。坐标北京,深耕Java生态多年,经历过从单体架构到微服务再到Serverless的完整演进过程。

评论 0