用实战告诉你,分布式事务到底怎么搞?
我是一个在互联网公司工作的后端开发者,过去几年里,我参与并主导过多个大型系统的重构和优化工作。从最初的单体应用到后来的微服务架构,系统拆分越来越细、模块越来越多,随之而来的最头疼的问题之一就是:跨服务的数据一致性问题。
尤其是在支付、订单、库存这些核心业务场景下,我们常常会遇到这样的需求:一个操作必须保证多个服务同时成功或同时失败。如果其中一个环节出错,其他已经执行的操作也得回退,否则就会出现数据不一致,最终影响用户体验,甚至带来财务风险。
这篇文章想跟你聊聊我在实际工作中,面对分布式事务这个问题时的一些思考、踩过的坑,以及我们最终选择的技术方案和落地效果。
背景介绍:为什么会有“分布式事务”的烦恼?

我们公司的核心业务系统,最初是基于单体架构搭建的。随着用户量增长和服务复杂度提升,我们逐步将系统拆分为多个独立的微服务模块,例如:
- 用户中心
- 订单服务
- 支付中心
- 库存服务
每个服务都有自己独立的数据库,使用 Spring Boot + MyBatis 开发,部署在 Kubernetes 上。整个系统通过 REST API 进行调用通信。
当我们要处理一笔订单时,流程大致如下:
- 订单服务创建订单;
- 库存服务扣减库存;
- 支付中心进行支付;
- 用户中心更新积分或者账户余额。
这四个步骤涉及四个不同的服务和数据库,而且它们之间是强依赖关系——只要其中一步失败,整个业务流程都得回滚。这时候你可能觉得:那加个事务就行了呗?可问题是,在分布式系统中,本地事务无法跨越服务边界。
于是我们遇到了一个很典型的问题:
如何在多个服务之间保证事务的一致性?
遇到的挑战:分布式事务不是个简单题

一开始,我们尝试手动维护状态,比如先记录日志,然后顺序执行各个服务接口,失败后再主动去补偿。但很快发现这种做法不可持续,因为:
- 日志逻辑复杂,难以调试;
- 出现网络异常或超时怎么办?
- 补偿逻辑容易漏掉边界情况;
- 不易扩展,一换需求就得大改代码;
- 没有统一的事务管理机制,缺乏灵活性。
举个例子:某次我们上线了一个优惠券核销流程,订单服务调用优惠券服务扣减额度时网络超时,订单服务以为失败了,但优惠券那边其实已经成功执行,结果用户没下单成功但优惠券却被扣掉了。这就导致大量客诉,运营还要做人工退款。
这让我们意识到:光靠自己写补偿逻辑,太容易出错了,需要一个更可靠、可控、易于维护的解决方案。
解决方案:我们选用了 Seata,它是我们的“分布式事务管家”
调研过后,我们决定引入开源项目 Seata 来解决分布式事务问题。
Seata 是阿里巴巴开源的分布式事务中间件,提供了一种叫 AT 模式(Auto Transaction Mode) 的解决方案。它本质上是在本地事务的基础上加上“全局锁”,并通过日志记录实现回滚与提交控制。
我们选择 Seata 的几个理由:
- 侵入性低:只需对数据库、SQL 和注解稍作改造即可接入;
- 性能较好:相比传统的两阶段提交协议(2PC),AT 模式在第一阶段直接提交本地事务,第二阶段异步清理;
- 社区活跃:官方文档完善,社区支持丰富;
- 适合微服务架构:天然兼容 Spring Cloud 生态;
- 具备容灾能力:即使某个服务宕机,也能依靠事务日志恢复。
实施过程:如何一步步接入 Seata?
第一步:架构调整
我们在系统中引入了一个名为 Seata Server 的角色,作为协调者(Transaction Coordinator)。每个服务节点安装 Seata 客户端 SDK,并连接到 TC。
整体结构变成了这样:
+------------------+ +------------------+
| Order Service |<----->| Seata Server |
+------------------+ +------------------+
|
v
+------------------+
| Inventory Service|<-----+
+------------------+ |
|
+------------------+ |
| Payment Service |<-----+
+------------------+ |
|
+------------------+ |
| User Center |<-----+
+------------------+
Seata Server 管理事务的全局状态,客户端负责执行本地事务,并上报状态。
第二步:服务改造
以订单服务为例,在创建订单的方法上添加了 Seata 的事务控制注解:
@GlobalTransactional(name = "create_order_tx")
public void createOrder(OrderDTO orderDTO) {
// 调用本地数据库插入订单
orderMapper.insert(order);
// 调用库存服务远程接口扣减库存
inventoryService.decreaseStock(orderDTO.getProductId(), orderDTO.getProductNum());
// 调用支付服务完成支付
paymentService.charge(orderDTO.getAmount());
// 调用用户中心更新积分
userService.updatePoints(order.getUserId(), orderDTO.getPoints());
}
这里的关键是 @GlobalTransactional 注解。这个注解标识的是一个全局事务入口,当方法被调用时,Seata 会自动开启一个全局事务,并为后续的服务调用生成唯一的 XID,传递给下游服务。
下游服务接收到请求后,也会自动注册到当前事务,并在本地数据库记录 undo_log(用于事务回滚)。等到主事务调用方 commit/rollback 时,Seata 再统一处理各服务的提交或回滚动作。
第三步:配置调整
每个服务都新增了如下的配置项(Spring Boot application.yml):
seata:
enabled: true
application-id: order-service
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
config:
type: file
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
此外,还需要对数据库做一些准备工作:
- 每个数据库都要增加一张
undo_log表,用于事务回滚。 - 修改数据访问层,确保所有 SQL 操作都在同一个数据库连接池中执行(避免死锁或隔离级别冲突)。
踩坑经验分享:别让这些坑绊倒你!
虽然 Seata 很强大,但在实际开发过程中我们也踩了不少坑,下面是一些关键点和教训总结:
1. 注意 SQL 语法限制
Seata 的 AT 模式对 SQL 有一定的限制,不能识别一些复杂的语句,比如嵌套子查询、联合 UPDATE 等。建议:
- 使用简单的 insert、update、delete 操作;
- 尽量避免动态 SQL;
- 如果确实需要用复杂语句,可以切换成 TCC 模式,自行编写 cancel 方法。
2. 数据库连接池设置不当引发死锁
我们在初期使用了 HikariCP,连接池大小设置为 5,但发现当并发较大时,经常卡死在获取 Undo Log 锁的地方。
解决办法:适当调大连接池大小,并且根据压测结果做参数调优。
3. XID 未正确透传导致事务不生效
有些时候,我们并没有把 XID 透传到下游服务,导致事务管理失效。
比如在 Feign 调用时,我们需要自定义拦截器将 XID 添加到 header 中:
@Configuration
public class SeataFeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (xid != null) {
template.header("XID", xid);
}
}
}
下游服务也要拦截请求头,还原 XID 上下文:
@Component
public class SeataContextFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String xid = httpRequest.getHeader("XID");
if (StringUtils.isNotBlank(xid)) {
RootContext.bind(xid);
}
try {
chain.doFilter(request, response);
} finally {
RootContext.unbind();
}
}
}
4. 幂等性和重试机制必须配合使用
在某些网络不稳定的情况下,RPC 可能会出现重复请求。如果没有幂等设计,可能导致库存多次扣减。
因此我们建议:
- 在关键接口设计中加入幂等字段(如流水号、交易ID);
- 对重复的幂等请求直接返回 success;
- 结合 Redis 缓存防重。
5. 不要过度依赖全局事务
有时候我们会为了图省事,给所有接口都加上 @GlobalTransactional,但实际上并不是所有业务都需要这么高的事务一致性。
建议结合业务场景,明确哪些流程必须严格一致(如支付、库存),哪些可以接受 eventual consistency(如日志记录、通知类操作),从而合理使用分布式事务。
效果总结:我们的收益是什么?
引入 Seata 后,我们取得了以下显著成效:
- 业务一致性增强:订单成功率提高约 5%,异常数据基本消失;
- 代码清晰易维护:不再需要自己写补偿逻辑,代码结构更干净;
- 运维成本降低:Seata 提供了可视化界面和日志追踪功能,方便排查问题;
- 开发效率提升:新业务接入分布式事务更快捷,节省时间;
- 系统可用性提升:通过合理的回滚机制,即使个别服务故障也不会造成大面积阻塞。
经验总结:几点建议送给各位同行
如果你也在面临分布式事务的问题,我想分享以下几个小建议:
- 不是所有地方都适合用全局事务:能接受最终一致性的场景尽量不用;
- 事务粒度要精细:一个事务不要太长,避免占用资源过多;
- 日志一定要完整:XID 要打印进日志,方便定位问题;
- 要有降级方案:比如事务失败后,走人工补偿通道;
- 技术不是银弹:引入任何框架前都要评估团队熟悉程度和学习成本。
说到最后,我其实有一个感受特别深的感悟:
分布式系统中的问题,很多时候都不是单纯靠一个框架就能解决的。解决问题的背后,是我们对系统设计的理解、对业务边界的把握、对错误容忍度的权衡。
Seata 是一把好工具,但它只是一个手段,真正重要的还是我们作为架构师或开发者如何去理解和应用它。
结语
分布式事务不是一个轻松的话题,但也不是高不可攀的难题。只要你理解了它的本质,选对了工具,再结合具体的业务场景去做调整,就一定能找到一个适合自己系统的最佳实践方案。
这篇文章讲完了我个人在工作实践中的一些经验和思考,希望对你有所帮助。如果你有不同的看法,或者也有自己的实战案例,欢迎留言交流!
作者备注:本文内容全部来源于本人亲身参与的项目经历,如有雷同纯属巧合。文中部分示例代码经过简化展示,实际生产环境请根据自身情况做适配和测试。

评论 0