从踩坑到填坑:我在分布式事务上的实战经验分享

前端说你再看
2025-06-17 13:26
阅读 281

引言:为什么分布式事务这么重要?

引言:为什么分布式事务这么重要?

刚进公司那会儿,我接手了一个电商系统的订单模块。当时系统已经跑了一阵子,用户量也不算小。最开始的架构比较简单,订单服务和库存服务各自独立部署,通过RPC调用进行通信。比如下单操作需要先扣减库存,再创建订单记录。

听起来挺合理的对吧?但问题来了:当扣减库存成功后,如果创建订单失败了怎么办?

这可就悲剧了,库存被扣了,订单却没有生成,用户肯定会投诉——而且数据还不一致。更麻烦的是,这种场景在高并发下特别容易出现,尤其是在网络不稳定、服务宕机等情况下,问题会被放大。

于是,我们团队不得不面对一个老大难的问题:如何在微服务架构下保证多个服务之间的数据一致性?

这就是今天要聊的话题:分布式事务


问题描述:我们的“经典”故障现场

问题描述:我们的“经典”故障现场

说个真实的例子。去年双11前压测期间,我们在模拟秒杀场景时发现一个严重问题:

用户A参与抢购活动,商品价格是50元。

  1. 库存服务检查有货,执行减库存(原子操作);
  2. 订单服务插入订单记录;
  3. 支付服务发起支付请求;

但在步骤2插入订单时超时了,虽然后续重试成功,但因为没有幂等性设计,导致用户实际被扣了两次钱!

这不是简单的业务错误,而是典型的分布式系统下的一致性问题

主要挑战包括:

  • 多个服务间的数据操作必须要么全部成功,要么全部回滚;
  • 系统需要容忍一定的延迟和部分失败;
  • 不同数据库无法直接使用本地事务;
  • 需要考虑性能开销,尤其是高并发场景。

我们不能接受这类数据不一致带来的损失,必须找出合适的解决方案。


解决方案:选择合适的技术方案

解决方案:选择合适的技术方案

我们团队最初尝试了几种方式:

1. 最终一致性(Eventual Consistency)

我们尝试通过异步消息队列来解耦服务,比如用 RocketMQ 发布事件,下游服务监听并处理自己的业务逻辑。

但问题是:如果中间任何一个环节失败了,如何回滚其他已发生的变更?

例如:库存已经被扣减了,但如果发完消息后应用宕机,其他服务根本不知道发生了什么,也无法主动回滚。

最终结论:适用于容忍一定不一致性的场景,但不能解决强一致性需求。


2. 使用 TCC(Try - Confirm - Cancel)

TCC 是一种补偿型事务机制,核心思想是:

  • Try:资源预留阶段(冻结库存);
  • Confirm:业务执行成功,确认资源占用;
  • Cancel:执行失败,释放资源;

这个方案看起来很理想,但我们实际开发过程中遇到了一些问题:

案例分享:

我们给库存服务加了个冻结字段,每次下单先做 Try(冻结库存),然后执行订单创建,最后 Confirm。

但随着业务复杂度增加,各种异常分支变多了,代码变得非常臃肿。Cancel 的实现也容易出错,特别是涉及多个服务的情况下,一旦 Cancel 失败还需要不断重试甚至人工介入。

最终结论:适合业务流程清晰且可以预估回退动作的场景,但复杂度较高,维护成本大。


3. 使用 Seata 进行全局事务控制

Seata 是阿里开源的一个分布式事务框架,支持 AT、TCC、Saga 和 XA 模式。

我们最终选择了它的 AT模式,因为它对业务代码入侵最小,几乎不需要修改现有逻辑,只需要加上注解即可开启分布式事务。

实现思路:

  • 在订单服务入口方法上添加 @GlobalTransactional 注解;
  • 各服务连接各自的数据库;
  • Seata 会在事务提交或回滚时自动协调所有参与方;
  • 底层基于 undo_log 表进行回滚操作,对 MySQL 的兼容性非常好。

优点很明显:

  • 开发效率高;
  • 基本无侵入性;
  • 对 DB 友好;
  • 社区活跃,文档齐全。

代码实践:简单看一个示例

为了给大家一个直观的感受,这里贴一个简化版的核心代码逻辑:

// 订单服务主入口
public class OrderService {

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    @Autowired
    private PaymentFeignClient paymentFeignClient;

    @GlobalTransactional(timeoutMills = 3000)
    public void placeOrder(OrderDTO orderDTO) {
        // 调用库存服务(Try阶段)
        inventoryFeignClient.deductInventory(orderDTO.getProductId(), orderDTO.getCount());

        // 插入订单记录
        orderRepository.save(orderDTO.toEntity());

        // 调用支付服务(Confirm/Canel由Seata自动处理)
        paymentFeignClient.charge(orderDTO.getUserId(), orderDTO.getTotalPrice());
    }
}

关键点说明:

  • @GlobalTransactional 注解开启了一个全局事务,Seata 会拦截这些调用,并记录前后镜像用于回滚;
  • 每个服务都配置了 Seata 的 client,指向 TC(Transaction Coordinator);
  • 所有 DB 操作都会自动注册为 RM(Resource Manager);
  • 如果任何一步抛出了异常,整个事务都会回滚。

踩坑经验:开发过程中遇到的真实问题

下面是我个人亲身经历的一些“血泪教训”,希望能帮大家少走弯路。

问题一:RM未正确注册导致事务失效

有一次上线新服务后,发现分布式事务没生效,查询 Seata 控制台才发现该服务的 RM 并没有注册上来。

原因排查: 原来该服务在初始化的时候启动太快,在 Seata 初始化完成之前就开始执行 DB 操作,导致事务上下文丢失。

解决办法:

  • 加一个初始化标志位,等待 Seata 初始化完成后才开放 DB 请求;
  • 或者在 Spring Boot 中将 Seata Starter 设置为启动项优先加载。

问题二:undo_log 数据过大影响性能

某天凌晨报警:某个数据库的 undo_log 表占用空间暴涨,导致主从延迟严重。

根本原因: 长时间运行的长事务较多,或者某些事务频繁提交失败导致多次重试,undo_log 积压过多。

解决方案:

  • 增加定时任务定期清理过期的 undo_log;
  • 优化事务提交失败后的重试策略,避免无限重试;
  • 升级 Seata 到 1.6+,自带清理功能(GC Worker);
  • 避免把耗时较长的操作放在全局事务中。

问题三:事务传播失败导致数据不一致

有一次我们发版后出现了数据不一致的情况。排查发现是 Feign 调用链中断,全局事务上下文没有正确传递。

根本原因:

  • 使用了 OpenFeign,默认不会透传 Seata 的 XID;
  • 若中间夹杂 HTTP 请求,而 Header 没有带上 XID,会导致事务断链;
  • 需要在 Feign Client 添加自定义拦截器,手动将 XID 透传下去。

解决方案: 写了一个 Feign Interceptor 自动注入 Seata 的 XID,确保事务传播链完整。


效果总结:上线后的收益与效果

自从引入 Seata 之后,我们系统的几个核心指标有了明显改善:

指标 上线前 上线后
分布式事务失败率 1.2% <0.05%
数据不一致发生次数 每周平均 3次 几乎无
事务平均响应时间 ~80ms ~120ms(增加了协调开销)
开发迭代速度 慢(需处理补偿逻辑)

虽然性能略微下降了一些,但从整体稳定性和维护成本来看,收益远大于成本。


经验分享:给开发者的几点建议

结合我的经验和踩过的坑,给正在考虑引入分布式事务的你几条建议:

✅ 选型要匹配业务场景

不同场景适合不同的方案:

  • 最终一致性 → 异步 + 消息队列;
  • 强一致性 + 结构清晰 → TCC;
  • 快速接入、减少改动 → Seata(AT模式为主);
  • 长周期任务、状态流转多 → Saga 模式。

别一味追求技术潮流,适合自己才是最好的。


✅ 性能永远是个重点

引入分布式事务必然会带来额外开销,尤其要注意以下几点:

  • 事务边界尽可能小,不要包裹无关代码;
  • 尽量不在事务内做 RPC/HTTP 请求,尤其是跨地域;
  • 监控事务响应时间和失败率,设置合理的熔断降级机制;
  • 提供快速回滚工具,防止问题扩散。

✅ 数据库设计也很关键

  • 表结构要有版本号/更新时间戳,用于幂等判断;
  • 重要操作建议记录日志、流水号,便于追踪;
  • undo_log 表单独建索引,加快清理速度;
  • 定期备份 undo_log 表内容以防万一。

✅ 接口设计要有兜底能力

  • 所有对外接口提供幂等性支持(如带 orderId 去重);
  • 每个服务暴露健康检查接口,便于 Seata 查询状态;
  • 要支持手动触发回滚或补单的功能;
  • 提供可视化工具或命令行工具,辅助运维人员处理异常事务。

✅ 生产环境运维注意事项

  • 部署 Seata Server 前务必要做容量评估;
  • 建议将 TC(事务协调器)部署成集群(至少3节点);
  • undo_log 表要做分区 + 定期清理;
  • 监控 TC、RM 的心跳健康状态;
  • 使用 Sentinel 控制 Seata 的 QPS,防止雪崩;
  • 要设置自动熔断策略,避免大面积雪崩。

写在最后:分布式事务不是万能药

其实这篇文章写了这么久,我也想说一句心里话:

分布式事务不是银弹。它只是帮你掩盖系统复杂性的手段之一。

真正决定一个系统健壮性的,是你对业务的理解、架构的设计、监控体系的完善程度,以及团队协同的能力。

引入分布式事务后,我们要做的还有很多,比如:

  • 更完善的自动化测试;
  • 更细致的异常上报机制;
  • 更智能的告警规则;
  • 更轻量的事务拆分;
  • 更强大的降级预案……

希望这篇来自真实项目的经验分享对你有所启发。如果你也在用 Seata 或者正在规划分布式事务架构,欢迎留言一起交流,我们一起踩坑、填坑、升级打怪!


📌 关注公众号【TechGrow】,获取更多干货文章和技术成长笔记。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝