分布式事务解决方案:我踩过的坑与实战经验总结
一、开篇:为什么我要写这篇文章?

作为一个后端开发团队的负责人,我在过去几年中主导或参与了多个微服务架构下的项目。其中有一个项目,让我印象特别深刻,因为它彻底刷新了我对分布式事务的理解。
那是一个金融行业的核心交易系统重构项目。原来的系统是单体架构,虽然稳定性不错,但随着业务发展,扩展性差、迭代效率低等问题逐渐暴露出来。于是我们决定引入微服务架构。然而,在拆分过程中,最大的技术难题就是——如何处理跨服务的数据一致性?
这个问题听起来很“经典”,也确实被无数文章讨论过。但在实际落地的时候,你才会发现,那些看似清晰的方案,到了具体场景下却变得异常复杂和脆弱。比如订单服务调库存服务,调支付服务,中间任何一个环节出错,整个业务流程就会出现数据不一致的风险。
这篇文章不是教科书式的理论讲解,而是一份基于真实工作场景的总结,我希望通过自己遇到的问题、踩过的坑以及最终的实践成果,给大家带来一些能直接套用的经验和思考。
二、项目背景:从单体到微服务,挑战来了

项目最初的目标很明确:将原有系统的订单、库存、支付等核心模块拆分为独立微服务,并提供统一的服务网关对外暴露接口。目标是提升系统可维护性和扩展能力,同时为后续接入更多合作伙伴做准备。
初期推进非常顺利:我们使用 Spring Cloud + Nacos 做服务治理,Redis 缓存热点数据,MongoDB 存储日志类信息。但真正遇到大问题的是在事务这一块——典型的下单场景需要依次完成:
- 创建订单(Order Service)
- 扣减库存(Inventory Service)
- 调起支付(Payment Service)
这三个服务之间没有任何共享数据库(当然也不能有),所以当某一步失败时,之前的操作必须回滚。这时候,我们就面临了一个经典的 分布式事务问题。
最开始我们尝试使用本地事务 + 简单的补偿逻辑来解决,但很快就被现实打了脸——系统上线一周内就出现了多笔数据不一致的问题:有的订单创建成功了但库存没扣;有的支付成功了但订单状态却是未付款。
这显然不能接受。我们需要一个更成熟的方案来应对这种高一致性的需求。
三、问题描述:分布式环境下事务管理之痛


当时摆在我们面前的主要问题包括:
- 缺乏强一致性保障:传统本地事务无法跨越多个服务
- 补偿机制复杂且易错:异步补偿难以追踪状态变更
- 性能瓶颈突出:部分方案存在严重的资源锁定
- 幂等性难以保证:网络重传、并发请求容易导致重复操作
我们一度考虑是否继续采用单体架构,但从业务角度出发,拆分已经是趋势,不能再退回去。
这时候我们意识到,必须系统性地评估现有的几种常见分布式事务方案,结合实际业务特性来选择最适合我们的那一套。
四、解决方案:从 TCC 到 Saga,最后选择了 Seata 框架
我们调研了以下几种主流方案:
1. 两阶段提交(2PC)
这个方案太老了,而且协调者故障会导致整个事务卡死,不适合我们这样对可用性要求极高的系统。
2. TCC(Try/Confirm/Cancel)
这个模式比较灵活,可以在业务层实现较强的控制力。例如:
- Try 阶段:预占资源(冻结库存)
- Confirm:执行正式操作(订单生成+库存扣减)
- Cancel:回滚所有操作
优点很明显:性能好,没有长时间资源锁定。但缺点也很致命——开发成本极高,每一个服务都要额外定义一套 C 和 C 的方法,还要处理幂等、重复回调等问题。
我们在一个小子系统上做了 PoC,发现代码量翻倍不说,调试也非常困难。对于业务迭代快速变化的项目来说,TCC 显得不够敏捷。
3. Saga 模式
Saga 是一种长事务编排模式,由一系列本地事务组成,每个事务都有对应的补偿动作。它非常适合像电商下单这种步骤明确、顺序性强的场景。
但它的问题是:一旦某个补偿失败,恢复过程可能需要人工介入,增加了运维成本。
4. 最终选择:Seata AT 模式 + 部分业务手动补偿
综合各种因素,我们最终采用了阿里巴巴开源的分布式事务框架 —— Seata,主要使用其 AT(Auto Transaction)模式,在必要地方辅以手动补偿机制。
这里简单介绍下 Seata 的 AT 模式:
- 依赖全局事务 ID(XID)
- 在本地事务中自动记录 undo log(前置镜像)
- 当有分支事务失败时,进行全局回滚
- 不需要开发者手动写 Confirm / Cancel 方法
Seata 的优势在于:
- 对业务侵入少
- 支持自动回滚
- 性能尚可(相比 2PC)
- 社区活跃,文档丰富,企业级案例多
我们最终搭建了一个独立部署的 Seata Server,集成在所有的业务服务中。
五、代码实践:关键配置与代码片段分享

以下是几个关键点的实现示例。
1. 引入依赖(Spring Boot + Seata)
<!-- application.yml -->
seata:
enabled: true
application-id: order-service
tx-service-group: my_test_tx_group
# 启动参数加上 -Dfile.encoding=UTF-8 -javaagent:seata-agent.jar
2. 全局事务入口
我们在主入口开启全局事务:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
@GlobalTransactional(name = "create_order_with_inventory_payment", rollbackFor = Exception.class)
public ResponseEntity<?> createOrder(@RequestBody OrderDTO dto) {
return ResponseEntity.ok(orderService.createOrder(dto));
}
}
3. 服务间调用注入 XID
我们使用 OpenFeign 做服务间调用,需要确保 XID 在链路中正确传递:
@Configuration
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (xid != null) {
template.header("XID", xid);
}
}
}
消费方接收到请求之后设置上下文:
@Component
public class XidFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String xid = request.getHeader("XID");
if (StringUtils.isNotBlank(xid)) {
RootContext.bind(xid);
}
try {
filterChain.doFilter(request, response);
} finally {
RootContext.unbind();
}
}
}
4. 数据库表结构小改动
Seata 的 AT 模式会自动生成 undo_log 表用于记录修改前后的状态:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
这个表在每个 DB 中都要建一份。
六、踩坑经验:这些坑我替你踩过了
1. Seata 版本兼容问题
一开始我们使用的是 Seata 1.4.x 版本,后来升级到 1.6.x,结果出现了一些诡异的回滚错误。建议大家选版本时要格外谨慎,尤其关注 Spring Boot、MyBatis 等生态的兼容性。
✅ 经验:建议使用 Seata 官方推荐的版本组合,并保持服务之间的版本统一。
2. 事务超时设置不合理
默认情况下,Seata 的全局事务最长等待时间为 60s。但我们有一个复杂的批量下单接口偶尔会超时,导致事务被强制中断。
✅ 解决办法:在
@GlobalTransactional(timeoutMills = 120_000)显式指定较长的超时时间,或者优化服务响应逻辑。
3. 多线程下 XID 丢失问题
由于订单服务内部用了多线程并行调用库存和支付服务,导致 XID 在子线程中丢失。
✅ 解决方式:自定义线程池,继承
DelegatingSecurityContextAsyncTaskExecutor或使用 Alibaba 提供的TransmittableThreadLocal来传递上下文。
4. 分布式锁和事务冲突
我们在某些高并发场景中使用 Redis 分布式锁来避免重复下单,结果导致事务挂起、阻塞甚至死锁。
✅ 注意事项:尽量避免在事务中加分布式锁,如必须使用,应将其放在事务边界之外,或使用更轻量级的同步策略(如乐观锁)。
七、效果总结:上线后的稳定性和收益
经过三个月的磨合和优化,这套基于 Seata 的分布式事务方案已经相对稳定。在高并发场景下(峰值 QPS 达 5000+),没有再出现数据不一致问题。我们也统计了一下核心指标:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 下单失败率 | 0.3% | <0.01% |
| 数据一致性投诉 | 平均每天 2~3 起 | 几乎为零 |
| 开发交付周期 | 平均 3天/功能 | 平均 2天/功能 |
更重要的是,整个团队对分布式事务的理解大大加深,不再谈“分布式”色变,而是敢于在微服务架构下设计复杂的业务流程。
八、经验分享:给正在挣扎的你几点建议
1. 不要为了微服务而微服务
分布式事务之所以难,是因为服务本身是“隔离”的产物。如果你的业务还没有复杂到必须拆服务的程度,完全可以先用模块化 + 接口抽象的方式过渡。
2. 技术方案选型要贴合业务特点
比如我们这种“下单”场景,属于有明确流程顺序、每一步都可逆的情况,用 Seata 很合适。但如果是日志聚合、事件驱动等弱一致性场景,可能完全不需要引入这么重的事务方案。
3. 关注可观测性,尽早引入监控机制
我们早期忽略了一件事:Seata 的事务状态是不可见的,出问题只能靠日志定位。后来我们引入了 Grafana + Prometheus 监控事务状态、超时情况,提前预警,减少人为排查时间。
4. 做好降级预案,关键时刻才能稳住阵脚
即使是最好的分布式事务系统,也不能保证永远不出问题。我们做了两套降级方案:
- 主动切换回“补偿+人工兜底”
- 自动触发“事务快照导出”,供运维核对
这两种方式帮我们度过了两次线上事故。
5. 多一点耐心和敬畏之心
分布式事务不是一个“一键搞定”的东西,它本质上是权衡的艺术。我们要学会根据系统规模、数据敏感性、性能要求等维度来做取舍。有时候不是没有方案,而是找不到适合你当前阶段的方案。
九、结尾:愿你在微服务的路上走得更远更稳
写这篇文章的过程中,我不止一次回忆起那段痛苦却又充实的日子。那时我们经常加班到深夜,争论该不该改某个配置,会不会引发连锁反应。但现在回头去看,正是这些问题和挑战,成就了更成熟的技术体系。
希望这篇来自一线实战的分享,能帮助你少走弯路。如果你还在分布式事务的泥潭中挣扎,请记住一句话:
“一切伟大的系统,都是从小处一步步构建起来的。”
愿你在这条路上,既脚踏实地,又能仰望星空。
如有疑问,欢迎留言交流 👋。

评论 0