[ 分布式事务,解决方案 01 ]

先确认你的两个问题

数据库事务 = 单库。 更准确说是单个数据库连接。MySQL 的 BEGIN/COMMIT 只对这一个连接上的操作生效,它根本不知道别的库、别的服务在干什么。

@Transactional 就是它。 Spring 只是帮你在方法进入时 BEGIN、正常返回时 COMMIT、抛异常时 ROLLBACK。本质上还是那一个数据库连接。项目里配了多数据源,一个方法也只能管住其中一个。


一个例子讲透:下单扣库存

场景 A:没拆分,一个库

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);                    // 订单表
    stockMapper.decrease(order.getSkuId(), 1);    // 库存表
}

同一个库、同一个连接,MySQL 保证这两条 SQL 要么都成、要么都回滚。这里完全不需要分布式事务。

场景 B:拆成两个服务、两个库

订单服务连订单库,库存服务连库存库。你天真地改成:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);        // 订单库
    stockClient.decrease(skuId, 1);   // ❌ HTTP 调库存服务
}

这段代码有三个致命问题

  1. 回滚不了对方。 库存扣成功了,但接下来 insert 抛异常,本地事务回滚了订单——库存却真的少了 1 个。远程操作不会跟着你回滚。
  2. 超时你不知道结果。 decrease 超时了,库存到底扣没扣?你无法判断。重试可能扣两次,不重试可能没扣。
  3. 长事务。 HTTP 调用可能几秒,这期间数据库连接和行锁一直被占着,并发一高连接池就爆。

核心结论:@Transactional 里绝对不能写远程调用。

场景 C:正确做法——本地消息表

思路是放弃"同时成功",改成"迟早都会成功"。分两段:

第一段:订单服务

在订单库里额外建一张 message 表(消息表),然后:

@Transactional  // 这两条都在订单库,一个本地事务能盖住
public void createOrder(Order order) {
    orderMapper.insert(order);
    messageMapper.insert(new Message(
        msgId,                     // 全局唯一ID
        "扣库存: skuId=1, qty=1",
        "待发送"
    ));
}
// 事务提交后,再由异步线程/定时任务把消息发到 MQ

这一步是整个方案的精髓: 消息表和订单表在同一个库,所以能用一个本地事务捆死。只要订单写成功了,"要扣库存"这件事就一定被记下来了,不可能丢。

第二段:库存服务

消费消息时:

@Transactional  // 这两条都在库存库,一个本地事务能盖住
public void onMessage(String msgId, Long skuId) {
    dedupMapper.insert(msgId);          // 去重表,msgId 建唯一索引
    stockMapper.decrease(skuId, 1);
}

如果这条消息被重复投递,insert(msgId) 会因为唯一索引冲突抛异常,整个事务回滚,库存不会被扣第二次。这就是幂等。

因为幂等了,所以 MQ 可以放心地无限重试,直到成功为止。整个链路里,每一段都是普通的本地事务,中间靠消息串起来。没有任何一个"跨库的大事务"存在。

image

中间态:用户看到什么

订单创建后到库存扣成功之间,可能有几百毫秒甚至几秒。这段时间订单状态是"处理中"。这就叫最终一致性——不是任何时刻都一致,而是保证过一会儿一定会一致。

业务上要接受这一点:显示"订单处理中",而不是骗用户说已完成。


你不熟的名词,对照着看

名词 在上面例子里对应什么
本地事务 @Transactional 圈住的那一段,单库内
幂等 库存服务插去重表,重复消息进不来
最终一致性 订单先"处理中",稍后才变"已完成"
本地消息表 订单库里那张 message
事务消息 RocketMQ 内置的功能,替你实现了消息表这套逻辑,少写代码
补偿 库存不足彻底扣不了 → 反向调用"取消订单"把订单关掉
2PC / XA 有个协调者指挥所有库一起提交,强一致但慢,基本不用
TCC 把扣库存拆成 Try(冻结)/ Confirm(真扣)/ Cancel(解冻),性能好但要写三套代码
Saga 多个步骤依次执行,某步失败就把前面的步骤逐个反向撤销

一句话对比

  • 本地事务:一个库说了算,能真正回滚。
  • 分布式事务:谁也说了不算,只能靠「记下来 + 一直重试 + 幂等保护 + 实在不行就补偿」,换来最终一致。

分布式事务不是更强的事务,是更弱的事务。 你为了拆分服务,付出的代价就是失去了强一致性。

posted @ 2026-09-05 15:42  十三山入秋  阅读(5)  评论(0)    收藏  举报