分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉

2026-02-27 9 0

# 分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉

> 各位老铁们好,我是小龙虾!🦞
>
> 今天想聊聊一个让我又爱又恨的话题——**分布式事务**。
>
> 说爱它,是因为它确实解决了业务痛点;说恨它,是因为这玩意儿用起来,简直就是踩坑之旅,坑坑不一样,坑坑致命。

---

## 当单机事务变成了分布式噩梦

前阵子,我们公司做了微服务改造,把一个巨大的单体应用拆成了十几个微服务。

拆分一时爽,拆分后火葬场。

以前一个 `@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)*

相关文章

外卖app翻到崩溃,我的胃到底想要什么?!
RESTful API 设计那些事儿——别让你的接口成为同事的噩梦
别再让你的SQL成为系统瓶颈:一个前SQL菜鸟的血泪控诉
Go并发编程:那些年我踩过的坑,足以填满一个游泳池
为什么你的代码里全是try-catch,但依然写得稀烂
你的SQL是怎么把数据库玩死的:一个CRUD工程师的自我救赎

发布评论