分布式事务解决方案:一次真实项目中的战斗实录
引言:分布式系统带来的挑战

作为一名有五年后端开发经验的程序员,我在多个大型系统中经历过不少技术“生死时速”。要说最难缠的问题之一,那一定有分布式事务的一席之地。
我所在的团队曾参与一个电商平台的核心系统重构。平台在业务扩张过程中,拆分出了订单服务、库存服务、支付服务等多个微服务。原本简单的下单流程,现在涉及多个服务间的数据一致性问题。最典型的一个场景是:
用户下单 → 订单服务创建订单 → 库存服务扣除库存 → 支付服务扣款。
这个过程要求要么全部成功,要么全不生效。一开始我们尝试了本地事务管理,但随着服务之间的调用加深,数据一致性逐渐失控。于是我们不得不认真面对一个老大难的问题:如何在分布式环境下保证事务一致性?
这篇文章就记录了那次项目中我们在分布式事务上的探索和实战经验。
问题描述:为什么说分布式事务很“坑”?

在项目初期,我们采用的是每个服务维护自己的数据库,服务之间通过 HTTP 接口或消息队列通信。这样的架构虽然松耦合,但也带来了事务边界模糊的问题。
举个例子:如果订单创建成功,库存也扣减了,但在支付环节失败了怎么办?这时候需要回滚前面的操作。但订单服务和库存服务都各自有自己的数据库,无法进行原子性操作。
我们当时遇到的问题包括但不限于:
- 接口异常重试导致重复处理(如库存被多次扣减)
- 最终一致性延迟明显(用户看到状态不一致的时间过长)
- 补偿逻辑复杂难以维护
- 性能瓶颈显著
这些问题让整个系统的稳定性和可用性都受到了严重影响。
解决方案:从 TCC 到 Saga 模式再到 Seata 的落地
第一阶段:TCC 尝试失败
我们第一个想到的是经典的 TCC(Try-Confirm-Cancel)模式。
思路是:
- Try 阶段:预检查并预留资源(比如冻结库存)
- Confirm 阶段:正式提交
- Cancel 阶段:逆向操作(解冻库存)
但我们很快发现几个痛点:
- 实现成本高,每一个步骤都要写三套逻辑
- 代码侵入性强,业务代码与事务控制代码混杂在一起
- 补偿机制容易出错,Cancel 失败会导致数据脏
而且我们的业务流程本身不是固定的,有些步骤存在可选分支(比如是否开启优惠券减免等),这使得 TCC 的设计更加复杂,甚至可以说不可控。
第二阶段:使用 Saga 模式简化流程
随后我们转向了 Saga 模式——一种基于事件驱动的异步事务模型。
Saga 的核心思想是:
- 把整个业务流程分解为多个本地事务
- 每个本地事务执行完成后发布一个事件,触发下一个事务
- 如果某一步骤失败,则沿着已经完成的步骤依次执行逆向补偿操作
这种方式的好处在于逻辑清晰、易于扩展,而且不需要全局锁,适合我们这种并发较高的电商系统。
但在实践中我们也发现:
- 整个链路非常依赖事件传递的可靠性(例如 Kafka 或 RocketMQ 的丢失、重复等问题)
- 逆向补偿的幂等性必须严格保证
- 日志追踪和人工干预的需求变强
第三阶段:引入 Seata 实现轻量级分布式事务
最终我们选择了阿里的开源项目 Seata,它提供了一个相对透明的方式实现分布式事务,底层采用 AT 模式(Auto Transaction),对应用层侵入少,适合我们这种多数据库、多服务的结构。
Seata 的原理大致如下:
- 全局事务协调者 TC(Transaction Coordinator)
- 资源管理器 RM(Resource Manager)
- 事务管理器 TM(Transaction Manager)
具体工作流程是:
- TM 向 TC 注册全局事务
- RM 在本地事务中注册分支事务,并提交至 TC
- 所有分支事务提交成功后,TC 提交全局事务
- 任一分支失败,TC 协调所有 RM 进行回滚
我们结合 Spring Boot + MyBatis + MySQL 使用 Seata 之后,效果明显提升:
- 事务一致性得到有效保障
- 开发人员无需手动编写复杂的补偿逻辑
- 可视化控制台提升了排查效率
代码实践:Seata 快速集成示例
以下是我们在项目中集成 Seata 的关键配置片段。
1. 添加 Seata starter
<!-- pom.xml -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
2. 配置 Seata 客户端
# application.yml
seata:
enabled: true
application-id: order-service # 当前应用ID
tx-service-group: my_test_tx_group # 事务组名,对应TC配置
service:
vgroup-mapping:
my_test_tx_group: default # 映射到TC集群
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
3. 启动 Seata Server(TC)
我们部署了一个独立的 Seata Server 集群,使用 Nacos 作为配置中心和注册中心。这部分可以参考官方文档搭建,略繁琐但社区资料丰富。
4. 在 Service 中使用全局事务注解
// OrderService.java
@GlobalTransactional
public void placeOrder(OrderDTO order) {
// 创建订单
orderMapper.insert(order);
// 扣除库存(远程调用)
inventoryService.decreaseStock(order.getProductId(), order.getCount());
// 支付处理(远程调用)
paymentService.pay(order.getPaymentAmount());
}
这里的关键在于 @GlobalTransactional 注解,一旦方法执行失败,Seata 会自动回滚所有已执行的数据库操作,前提是这些操作都在支持的数据库及其客户端下运行。
踩坑经验:别让“看起来简单”害了你
尽管 Seata 大大降低了分布式事务的使用门槛,但在实际应用中我们还是踩了不少坑,下面分享几个典型案例。
1. 数据库连接池不够用导致卡死
我们在压测中发现 Seata 事务频繁超时,分析日志后发现是因为默认的 Druid 连接池不足,特别是在并发场景下,事务分支数量较多,连接池耗尽。
解决办法:扩大数据库连接池最大连接数,适当调整 max-pool-size 和 min-pool-size,并监控连接池使用率。
2. 未处理幂等性导致重复扣款
在一次网络波动后,支付服务收到了两次相同的请求,导致用户账户被扣了两笔钱。
原因分析:虽然 RPC 调用设置了重试机制,但由于未对幂等性做校验,导致 Seata 回滚后仍然有部分操作被执行。
解决办法:我们在每个操作前加入唯一标识符(如 requestId),并在数据库记录该标识,防止重复操作。
3. 分支事务回滚失败导致数据不一致
某些情况下,Seata 的回滚操作执行失败,数据库状态出现异常。
解决办法:增加事务补偿监听器,在 Seata 回滚失败时触发人工干预流程,并记录日志用于后续回补。
效果总结:业务稳定性的全面提升
在引入 Seata 并优化相关模块后,系统的整体表现有了明显改善:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 事务成功率 | 92% | 99.5% |
| 系统平均响应时间 | 180ms | 135ms |
| 错误回滚率 | 3.5% | 0.2% |
| 人工介入次数/周 | 5+次 | 0~1次 |
更重要的是,团队内部的协作效率提高了很多,业务代码不再需要掺杂大量事务处理逻辑,真正做到了“关注点分离”。
经验分享:给正在踩坑的你几点建议
回顾这次经历,我想给同样在折腾分布式事务的兄弟们几点建议:
不要一上来就想着完美解决分布式事务。先搞清楚你的系统到底有没有真正的刚性一致性需求。很多场景其实可以通过最终一致性 + 冲突检测来解决。
选择适合团队的技术方案。TCC 很强大但实现成本高;Saga 更灵活但运维成本高;Seata 使用简便但需注意版本兼容性。没有银弹,只有合适不合适。
一定要做幂等性设计。无论是消息消费、RPC 调用还是事务补偿,幂等都是基础。你可以通过唯一 ID + Redis 缓存 + DB 去重来做。
日志、埋点、监控必须做好。Seata 控制台虽好,但生产环境还是要自己有一套事务追踪体系。否则出问题很难快速定位。
不要忽视性能开销。Seata 的 AT 模式会带来一定的性能损耗(大约 20% 左右),如果你追求极致性能,可以在热点路径上做降级处理。
写在最后:分布式事务,不止是技术问题
说实话,分布式事务不仅仅是一个技术难题,它更像是一场关于权衡的艺术。你要在一致性、可用性、性能、开发效率之间找到那个微妙的平衡点。
在这个项目中,我学到最重要的一课就是:技术服务于业务。有时候,为了一个极限场景去牺牲大部分情况下的易维护性和稳定性,反而是得不偿失的。
如果你也在和分布式事务“搏斗”,不妨静下来想想:是不是真的需要这么强的一致性?有没有更好的折中方式?
希望这篇来自真实战场的经验能帮你在路上少走些弯路。共勉!

评论 0