分布式事务的那些年,那些事

云端小木屋
2025-06-16 13:22
阅读 4031

一、背景:从“小事”演变成“大事”

一、背景:从“小事”演变成“大事”

记得那是我加入现在这家公司的第二个月。我们正在上线一个全新的积分商城系统,用户可以用平台积分享换各种实物和虚拟商品。系统本身不算复杂,订单中心负责下单,库存中心控制库存扣减,积分账户中心管理积分余额。一切看起来都挺简单,直到上线前的压力测试阶段。

那天晚上,压测工具跑完之后,技术主管拿着监控图脸色铁青地过来说:“我们的数据对不上了。”
——有用户的积分被扣了两次,有的商品库存居然变成了负数。

更糟的是,我们并没有一个统一的数据一致性检查机制。这个问题在本地事务中几乎不会出现,但在微服务架构下,每个模块独立部署,各自维护自己的数据库,这就意味着:我们要面对分布式事务的问题了。

于是,问题来了:

  • 订单创建成功了,但积分没扣掉怎么办?
  • 积分扣了,库存不足了,回滚能自动触发吗?
  • 如果某一步失败,其他服务怎么知道该回滚还是重试?

这些问题逼着我们迅速进入一场关于“分布式事务解决方案”的学习与实践之路。而这段经历,让我深刻体会到,在实际业务场景中,分布式事务不是理论模型那么简单,而是需要权衡多方因素的工程决策。

这篇文章我会结合我们当时的项目背景,讲讲当时踩过的坑、用过的方案,以及我们最终落地的“折中又实用”的方式。


二、项目背景与挑战:积分商城的高并发压力

二、项目背景与挑战:积分商城的高并发压力

我们这套积分商城主要提供以下几个核心功能:

  1. 用户查看商品详情;
  2. 下单并使用积分兑换;
  3. 库存扣减;
  4. 订单状态变更;
  5. 发货通知等后续流程。

整体采用 Spring Cloud 框架,服务之间通过 RESTful 接口通信,数据库使用 MySQL(分库分表) + Redis 缓存,消息队列选用 Kafka。

关键问题

在设计初期,大家想当然地认为,只要订单服务调用积分服务、库存服务的时候加上 try-catch 就行了,出错的话手动补偿。但现实远比理想骨感。

我们在一次模拟真实场景的压测中发现:

  • 并发达到 300QPS 左右时,就开始出现不一致:
    • 积分扣了,订单却未生成;
    • 库存减少了,用户却没有收到确认订单;
  • 更严重的是,某些操作执行了一半就中断,没有明确结果,只能靠人工核对处理;
  • 日志混乱,追踪困难。

这说明我们当前的服务间交互存在严重的原子性缺失和事务边界混乱问题。


三、解决方案:从本地事务到 Seata 的渐进式演化

三、解决方案:从本地事务到 Seata 的渐进式演化

最初我们尝试了几种典型的分布式事务解决方案,并进行了对比分析。

1. 本地事务+补偿机制(TCC 雏形)

我们最开始尝试手动编写“补偿逻辑”,也就是所谓的 TCC(Try, Confirm, Cancel)模式的一个简化版本:

  • Try 阶段:冻结积分,预减库存;
  • Confirm:提交订单,真正扣除积分和库存;
  • Cancel:某个环节失败时,回滚之前的操作。

实现过程大概是这样的:

// 伪代码
public void placeOrder(OrderRequest request) {
    try {
        inventoryService.reserve(request.getProductId(), request.getCount());
        pointService.frozenPoints(userId, pointsNeeded);
        
        orderService.createOrder();
        
    } catch (Exception e) {
        log.error("下单失败", e);
        pointService.restoreFrozenPoints(userId, pointsNeeded);
        inventoryService.releaseReservedInventory(productId, count);
        throw new OrderCreateFailedException();
    }
}

这种方式看上去可行,但带来了几个大问题:

  • 代码耦合度高:业务逻辑与事务处理混在一起,难以维护;
  • 补偿逻辑需要手动实现,容易出错;
  • 在并发场景下,无法保证幂等性和事务隔离性;
  • 如果 Cancel 失败,还得再补一个补偿动作……

这种方案勉强支撑了一段时间,但我们很快意识到这不是长久之计。


2. 引入 RocketMQ 事务消息(异步解耦)

后来我们考虑引入 RocketMQ 的事务消息机制,将积分变动作为一条事务消息投递到 MQ,只有确认本地事务完成才会真正消费。

虽然这种方式实现了异步化,但也有明显的局限:

  • 需要自己维护事务日志的状态机;
  • 跨服务事务恢复依赖定时任务兜底,响应延迟高;
  • 整体实现依旧复杂,特别是异常分支太多。

最终决定不再深入这个方向,转而去研究更成熟的中间件方案。


3. 最终选择:Seata + AT 模式

我们团队在调研一段时间后选择了阿里开源的 Seata(前身是 Fescar),它提供了一个比较完整的分布式事务解决方案。

我们采用了其中的 AT 模式(Auto Transaction mode),其特点是:

  • 只需对数据源做轻量代理,无需修改业务逻辑;
  • 自动记录 undo_log 实现回滚;
  • 基于两阶段提交协议实现全局一致性;
  • 支持常见的数据库类型(MySQL, Oracle 等);
  • 有一定的性能损耗,但可控。

配置示例

以下是我们在 Spring Boot 中集成 Seata 的关键配置:

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  data-source-proxy-mode: AT

并且,我们在需要开启分布式事务的方法上添加 @GlobalTransactional 注解:

@Override
@GlobalTransactional
public void placeOrder(OrderRequest request) {
    inventoryService.reduceInventory(request.getProductId(), request.getCount());
    pointService.deductPoints(request.getUserId(), request.getPoints());
    orderMapper.insert(order);
}

这样就能实现跨多个服务的数据一致性。


四、实施过程中遇到的坑

四、实施过程中遇到的坑

虽然 Seata 提供了良好的封装,但在实践中我们也遇到了不少问题。

1. 数据库必须支持 undo_log

我们早期用的是一套共用的 MySQL 实例,不同的服务共享同一个 schema。Seata 要求每个数据库实例都要有一个 undo_log 表,否则会报错。

解决办法:

  • 给每个服务单独建立数据库 schema;
  • 手动在每个 schema 中添加 seata 所需的 undo_log 表;
  • 使用 Flyway 自动初始化这些表结构。

2. 死锁问题一度让系统崩溃

由于我们启用了 Seata 的自动事务协调器,当多个订单同时操作同一商品库存时,出现了死锁现象。表现为:

  • 多个线程互相等待资源释放;
  • Seata 事务卡住,导致线程池被耗尽;
  • 整体系统不可用。

原因:

  • Seata 的 AT 模式本质上是加了全局锁机制,对于高并发更新热点资源的情况很容易发生冲突。

解决办法:

  • 对热门商品进行缓存预减库存(前端防超卖);
  • 使用 Redis 进行库存预占,减少数据库并发写压力;
  • 引入乐观锁控制并发更新;
  • 必要时进行服务降级,限制 Seata 事务嵌套深度。

五、最终效果与收获

经过几轮调整,我们最终将整个系统的分布式事务稳定性提升到了可用级别:

  • QPS 提升至 1000 以上,数据一致性达标;
  • 故障率降低为每小时不到一次;
  • 开发效率提升,业务逻辑和事务处理实现了解耦;
  • 异常处理机制完善,可实时定位事务状态;
  • 同时也接入了 Prometheus 和 Grafana 做了事务链路监控。

更重要的是,我们逐步建立了以下几个开发规范:

  • 所有涉及跨服务更新的数据操作必须启用 Seata;
  • 服务间通信必须设置合理的超时时间;
  • 所有事务入口点要有日志埋点;
  • 定期清理 Undo_log 表(保留 7 天即可);
  • 异常情况下要有报警机制联动值班人员。

六、给读者的一些建议

如果你也在考虑如何应对分布式事务,这里是我的一些实战建议:

✅ 架构层面提前考虑

  • 不要等到上线后再去补分布式事务的设计,那样代价太高。
  • 如果你的系统将来会上微服务,请尽早评估是否会有强一致性需求。

✅ 不要盲目追求“完美方案”

  • 没有所谓“最优解”,只有“最适合你业务场景的方案”。
  • 像 Seata 这类框架虽然成熟,但也要根据实际情况取舍,比如性能开销、锁竞争等问题。

✅ 分布式事务 ≠ 万能钥匙

  • 对于非关键路径上的数据更新(如日志、统计类操作),尽量使用异步或最终一致性方案;
  • 对于订单、支付等核心业务才考虑引入事务机制。

✅ 监控和告警必不可少

  • 使用 SkyWalking 或 Jaeger 做分布式链路追踪;
  • 搭配 Prometheus + AlertManager 做关键指标告警;
  • 设置事务超时阈值并告警。

七、结语:分布式事务这条路,走得艰难却不后悔

从一开始的懵懂无知,到现在可以坦然面对复杂的事务协调问题,我和我的团队走过了一段非常真实的成长旅程。

我始终相信一句话:任何伟大的架构,都是从小问题里长出来的。

如果你也正走在微服务的路上,也许会像我一样,被某个简单的下单接口折磨得怀疑人生。但别怕,多试试,总会找到一条合适你的方式。

希望这篇来自一线工程师的真实经验分享,能够帮你少走一点弯路。

感谢你的阅读,如果你有类似经验,欢迎留言交流!


💬 文章作者:一名热爱编码、热衷于构建高性能系统的 Java 工程师
🧠 技术栈:Java/Spring/MySQL/Redis/Kafka/Seata
📍 城市坐标:杭州
🕒 写作时间:深夜两点,咖啡已凉,文章未完

评论 0

最热最新
暂无评论
云端小木屋Lv.1
0
影响力
0
文章
0
粉丝