从本地事务到分布式事务
- 从本地事务到分布式事务
-
- [1. 从最简单的转账开始](#1. 从最简单的转账开始)
- [2. 本地事务为什么能做到?](#2. 本地事务为什么能做到?)
- [3. 微服务出现以后,事情变了](#3. 微服务出现以后,事情变了)
- [4. 为什么不能直接 rollback?](#4. 为什么不能直接 rollback?)
- [5. 更麻烦的是:网络不可靠](#5. 更麻烦的是:网络不可靠)
- [6. 那我重试不就好了?](#6. 那我重试不就好了?)
- [7. 分布式事务其实有两条路线](#7. 分布式事务其实有两条路线)
-
- [路线 A:大家一起提交](#路线 A:大家一起提交)
- [路线 B:允许暂时不一致,最后修正](#路线 B:允许暂时不一致,最后修正)
- [8. 用一个项目贯穿后面的课程](#8. 用一个项目贯穿后面的课程)
从本地事务到分布式事务
先记住一句话:
分布式事务,本质上是在多个独立事务资源之间,解决一个业务操作的一致性问题。
1. 从最简单的转账开始
假设银行转账:
text
张三账户 -100
李四账户 +100
如果两个账户都在同一个 MySQL 数据库,可以这样做:
sql
BEGIN;
UPDATE account
SET balance = balance - 100
WHERE user_id = 1;
UPDATE account
SET balance = balance + 100
WHERE user_id = 2;
COMMIT;
如果第二条 SQL 失败:
sql
ROLLBACK;
于是两条 SQL:
text
要么全部成功
要么全部失败
这就是本地事务。
2. 本地事务为什么能做到?
因为数据库给我们提供了 ACID。
text
A:Atomicity 原子性
C:Consistency 一致性
I:Isolation 隔离性
D:Durability 持久性
现阶段最需要理解的是 Atomicity(原子性)。
例如:
text
事务开始
① 创建订单
② 扣库存
③ 扣余额
事务提交
如果执行到第 ③ 步失败:
text
① 创建订单 → 回滚
② 扣库存 → 回滚
③ 扣余额 → 失败
最终数据库恢复到事务开始之前。
3. 微服务出现以后,事情变了
现在做一个电商系统:
text
用户下单
│
┌─────────┼─────────┐
↓ ↓ ↓
订单服务 库存服务 支付服务
│ │ │
DB1 DB2 DB3
用户购买一台 5000 元的电脑。
业务流程:
text
① 创建订单
↓
② 扣减库存
↓
③ 扣款 5000
假设运行结果:
text
订单服务
创建订单成功 ✅
↓
库存服务
扣库存成功 ✅
↓
支付服务
扣款失败 ❌
现在发生了非常麻烦的事情:
text
订单:已创建
库存:已经 -1
支付:没有成功
数据已经不一致了。
4. 为什么不能直接 rollback?
这是理解分布式事务最关键的一步。
订单服务的代码可能是:
java
@Transactional
public void createOrder() {
orderMapper.insert(order);
stockClient.deduct();
paymentClient.pay();
}
很多初学者容易产生一个误解:
我加了
@Transactional,paymentClient.pay() 抛异常,整个方法回滚不就行了吗?
不行。
因为:
text
@Transactional
│
↓
只能控制订单服务自己的数据库事务
│
↓
DB1
它控制不了:
text
库存服务 → DB2
支付服务 → DB3
因此可能发生:
text
orderMapper.insert()
↓
DB1 写入成功
stockClient.deduct()
↓
DB2 扣库存成功
paymentClient.pay()
↓
失败
↓
@Transactional
回滚 DB1
最后:
text
DB1:订单回滚了 ✅
DB2:库存已经扣了 ❌
DB3:没有扣钱 ❌
本地事务只能保证自己的数据库,无法天然保证整个调用链。
这就是我们需要分布式事务的重要原因。
5. 更麻烦的是:网络不可靠
分布式事务真正困难的地方不只是"多个数据库"。
而是:
网络会让你不知道对方到底成功了还是失败了。
例如订单服务调用支付服务:
text
订单服务
│
│ pay(5000)
↓
支付服务
订单服务等待了一段时间:
text
TimeoutException
问题来了:
支付到底成功没有?
存在两种可能。
情况 A:
text
请求根本没到支付服务
订单 ───X──→ 支付
支付失败。
但是还可能是情况 B:
text
订单 ─────→ 支付
扣款成功 ✅
│
│ response
X
网络断了
实际上支付服务已经:
text
balance = balance - 5000
只是响应没有回来。
于是订单服务看到:
text
TimeoutException
但真实情况是:
text
支付已经成功
这叫:
结果不确定性。
这是理解分布式系统非常重要的一个思想:
超时 ≠ 失败。
6. 那我重试不就好了?
很好,这马上引出了第二个核心问题。
订单服务:
java
try {
paymentClient.pay(5000);
} catch (TimeoutException e) {
paymentClient.pay(5000);
}
第一次其实已经成功:
text
余额 -5000
只是响应丢失。
于是你重试:
text
余额再次 -5000
用户买了一台电脑:
text
扣款:10000 元
事故出现。
因此我们马上需要另外一个知识:
幂等
例如支付请求必须携带:
text
requestId = PAY_20260914_00001
第一次:
text
PAY_20260914_00001
↓
扣款 5000
↓
记录:已经处理
第二次又收到:
text
PAY_20260914_00001
↓
发现已经处理
↓
不再扣款
↓
返回之前结果
于是:
text
请求 1 次
请求 10 次
请求 100 次
业务结果仍然一样
这就是幂等性。
所以你现在应该开始发现:
text
分布式事务
并不仅仅是
"怎么 rollback"
它实际上涉及一整套问题:
text
网络超时
↓
结果不确定
↓
需要重试
↓
重试产生重复执行
↓
需要幂等
↓
部分服务已经成功
↓
可能需要补偿
↓
补偿也可能失败
↓
继续重试 / 对账 / 人工兜底
这条链非常重要。
7. 分布式事务其实有两条路线
接下来整个分布式章节专栏都围绕这两种思想展开。
路线 A:大家一起提交
核心思想:
要成功大家一起成功,要失败大家一起失败。
典型方案:
text
XA
2PC
Seata XA
Seata AT
可以粗略理解为追求更强的一致性。
路线 B:允许暂时不一致,最后修正
例如:
text
订单创建成功
库存扣减成功
积分增加失败
没有必要把订单取消。
可以:
text
积分失败
↓
重试
↓
还是失败
↓
继续重试
↓
最终成功
订单创建后 500ms:
text
积分:0
过了 2 秒:
text
积分:+500
中间短暂不一致,但最终一致。
这就是:
最终一致性。
典型技术:
text
MQ
事务消息
本地消息表
Transactional Outbox
Saga
补偿机制
8. 用一个项目贯穿后面的课程
后面我们可以一直使用一个电商项目:
text
mall
│
├── order-service
│ └── MySQL
│
├── stock-service
│ └── MySQL
│
├── payment-service
│ └── MySQL
│
├── coupon-service
│ └── MySQL
│
└── points-service
└── MySQL
用户下单:
text
createOrder()
│
↓
创建订单
│
↓
扣减库存
│
↓
使用优惠券
│
↓
支付
│
↓
增加积分
后面我们分别思考:
text
支付失败怎么办?
库存扣了,订单失败怎么办?
MQ 消息丢了怎么办?
消息重复消费怎么办?
服务突然宕机怎么办?
Cancel 比 Try 先到怎么办?
数据库成功、MQ 失败怎么办?
然后你就会发现:
2PC、TCC、Saga、Outbox、MQ、幂等这些技术,都是为了解决这些具体问题,而不是为了背概念。
你需要真正记住 5 件事
- 本地事务只能天然管理自己的事务资源。
- 跨服务、跨数据库以后,会产生部分成功、部分失败。
- 分布式环境存在结果不确定性 ,尤其要记住:超时不等于失败。
- 网络异常必然带来重试 问题,重试又要求我们考虑幂等。
- 分布式事务不只有"全体回滚"这一条路,还可以通过重试和补偿实现最终一致性。
若有转载,请标明出处:https://blog.csdn.net/CharlesYuangc/article/details/165324033