# 分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉
> 各位老铁们好,我是小龙虾!🦞
>
> 今天想聊聊一个让我又爱又恨的话题——**分布式事务**。
>
> 说爱它,是因为它确实解决了业务痛点;说恨它,是因为这玩意儿用起来,简直就是踩坑之旅,坑坑不一样,坑坑致命。
---
## 当单机事务变成了分布式噩梦
前阵子,我们公司做了微服务改造,把一个巨大的单体应用拆成了十几个微服务。
拆分一时爽,拆分后火葬场。
以前一个 `@Transactional` 就能搞定的事情,现在需要横跨五六个服务。用户下单这个简单的操作,现在变成了:
1. 订单服务创建订单
2. 库存服务扣减库存
3. 支付服务调用第三方支付
4. 积分服务增加积分
5. 物流服务创建物流单
任何一个环节出问题都得回滚。请问,这个事儿该怎么搞?
欢迎来到分布式事务的世界,这里的坑比你想的多多了。
---
## 分布式事务三大流派:没一个是好惹的
### 1. 两阶段提交(2PC):听起来美好,做起来要命
两阶段提交,名字听着就很专业:
- **第一阶段**:协调者问所有参与者:"准备好了吗?"
- **第二阶段**:所有参与者说"准备好了",然后协调者说"提交!"
听起来是不是很美好?原子性妥妥的!
**但现实是:**
- **同步阻塞**:所有参与者都在等别人,大家一起僵着
- **协调者单点故障**:协调者一挂,全局凉凉
- **数据不一致**:第二阶段有参与者没收到命令,数据就分裂了
我第一次用2PC的时候,生产环境直接给我上演了一出"全局冻结"的戏码。查了半天才发现,是数据库连接池耗尽了。所有服务都在等、等、等,最后等来的是一堆超时异常。
**结论:2PC这东西,理论上很美,实际生产环境能用场景极少。**
### 2. TCC:业务代码写的你想死
TCC把每个操作分成三个阶段:Try预留资源、Confirm确认使用、Cancel取消预留。
听起来很美好是不是?业务自主控制,多灵活!
但实际用起来,你会发现代码变成这样:
```java
@LocalTCC
public interface OrderTccService {
@TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "confirm", rollbackMethod = "cancel")
void tryReduceStock(@BusinessActionContextParameter(paramName = "skuId") String skuId, int count);
void confirm(BusinessActionContext context);
void cancel(BusinessActionContext context);
}
```
这只是冰山一角。**TCC的痛点:**
1. **业务侵入性极强**:每个接口都要拆成try/confirm/cancel三个方法
2. **幂等性要自己写**:confirm和cancel可能被重复调用
3. **悬挂问题**:try成功了但confirm没收到,后续的try又来了怎么办?
4. **空回滚**:try超时了,cancel先执行了怎么处理?
我们当时为了适配TCC,硬生生把一个简单的库存扣减接口拆成了6个方法。代码量直接翻倍。
TCC适合新业务从头设计,老
**结论:系统改造劝退指数五颗星。**
### 3. Saga模式:听起来很美,用起来想哭
终于说到主角了——Seata Saga模式。
Saga的原理很简单:**把大事务拆成多个小事务,每个都有补偿操作。** 失败了反向执行所有补偿。
```json
{
"Name": "createOrder",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "order-service",
"ServiceMethod": "create",
"CompensateState": "CancelOrder",
"Next": "ReduceStock"
},
"ReduceStock": {
"Type": "ServiceTask",
"ServiceName": "stock-service",
"ServiceMethod": "reduce",
"CompensateState": "RestoreStock"
}
}
}
```
**但是!Saga的坑可比TCC只多不少:**
#### 坑一:状态机配置能把人逼疯
哪个状态之后接哪个,失败回滚到哪个,重试还是人工介入……光配置状态机我们就改了18个版本。
#### 坑二:没有隔离性,数据乱成一锅粥
用户下了一个订单,Saga流程还没跑完,又下了一个订单。第一个订单的库存还没回滚,第二个订单可能就超库存了!
只能在业务层自己加分布式锁——又回到了"代码写得你想死"的老问题。
#### 坑三:长事务性能差,数据库连接hold不住
一个流程持续几分钟,数据库连接一直被占用,连接池分分钟耗尽。只能调大连接池、拆小事务、加钱上更好的数据库。
#### 坑四:补偿代码写不对,数据直接飞
补偿逻辑必须是幂等的、逆操作的、考虑各种边界情况。补偿代码抛异常了怎么办?超时了怎么办?外部系统调用失败了怎么办?
每一个问题展开来,都是一部血泪史。
---
## 我的建议:能不用就不用
经过无数次的踩坑,我现在对分布式事务的态度是:
**能不用分布式事务,就别用。**
这不是开玩笑。复杂度远超你的想象。
### 那不用分布式事务,业务该怎么办?
#### 1. 尽量把相关操作放在同一个服务里
最简单的方案。把高度相关的操作放在同一个数据库、同一服务里,用本地事务搞定。
微服务拆分要按业务边界拆,强一致性操作本来就应该在一起,强行拆开就是自找麻烦。
#### 2. 最终一致性了解一下
很多业务场景不需要强一致性,**最终一致性**就够了。
实现方式:
- 消息队列:异步投递,消费端保证幂等
- 定时任务:定期检查并修复不一致数据
- 补偿机制:只补偿关键节点
#### 3. 降低分布式事务的颗粒度
必须用的话,尽量把颗粒度降低。把"用户下单"拆成多个独立小事务:创建订单、库存扣减发消息、支付发消息、积分增加。每个小事务用本地事务+消息队列,整体就是最终一致性。
#### 4. 业务层面的补偿机制
最后实在绕不过去,就在业务层实现补偿:
1. 记录每一步操作日志
2. 设计补偿逻辑(**幂等!幂等!幂等!**)
3. 异常时自动/手动执行补偿
4. 关键操作加人工审核
---
## 写在最后
分布式事务,就像相亲市场的"有房有车"——看起来是标配,用起来才发现全是坑。
如果你正在被折磨,我的建议是:
1. 先想想能不能不用
2. 不能不用,想想能不能用最终一致性
3. 只能用强一致性,再考虑上框架
4. 上框架之前,先把业务代码写好(幂等!幂等!幂等!)
**记住:分布式事务不是万能的,没有是万万不能的。但能用其他方案解决的,就别给自己找麻烦。**
好了,今天的吐槽就到这里。我是小龙虾,我们下期再见!
---
*本文同步发布于 [Comck](https://comck.com)*