说起分布式事务,做后端的朋友应该都不陌生。我最早接触这个场景是在一个订单系统里:用户下单,订单服务要写订单表,库存服务要扣减库存。单体架构时这俩在同一个数据库里,一个本地事务就解决了。可服务一拆、数据库一分,原本一个事务能搞定的事,硬生生变成了跨服务的“分布式事务”。线上时不时就出现订单生成了但库存没扣、或者库存扣了但订单没建成的情况,数据对不上,用户来投诉,研发就得连夜排查。今天我就围绕这个场景,把这几年在分布式事务上踩过的坑、用过的方案、以及Seata背后的原理,一次性讲透。
这篇文章适合所有正在做微服务拆分、或者被跨库数据一致性折腾过的后端工程师。不管你是刚接触分布式事务的新手,还是已经在用Seata但没弄明白它到底是怎么做到的,我建议你花十几分钟,从头到尾看一遍。你会发现,看透了原理之后,选型和技术方案落地都会变得非常简单。
1. 分布式事务是怎么冒出来的:从单库到微服务的那道坎
1.1 单体时代为什么没人纠结分布式事务
先聊一个很实在的问题:在单库时代,为什么没有“分布式事务”这种说法?
因为所有业务数据都在同一个数据库里。用户下单,订单表、库存表、账户表全在一台MySQL上,你用一个 @Transactional 注解包住Service方法,数据库自带的ACID特性就能保证:要么全部操作成功,要么全部回滚。整个过程里,事务的隔离、锁、提交、回滚全部由数据库内部完成,应用层只负责声明“这里需要事务”。
这套模型最让人省心的地方在于——你不需要关心一致性是怎么保证的。数据库帮你扛住了并发、异常、崩溃恢复这些所有的脏活累活。
但微服务架构一上来,事情就变了。订单服务只管订单库,库存服务只管库存库,两个库可能部署在不同的机器上,甚至数据源都不一样。你没法用一个数据库事务去同时控制两个独立的数据源。于是“事务”这个概念被迫从数据库层面,提升到了应用和网络层面。
1.2 微服务拆分后,订单和库存“分家”了
我举个例子,你现在有一个标准的“下单”流程:
- 订单服务:写入订单表,生成一条待支付订单
- 库存服务:扣减库存数量
- 支付服务:冻结或扣除用户金额
在单体系统里,这三件事在一个库、一个事务里完成。但拆成微服务后,每个服务持有自己的数据库,服务之间的调用走的是 HTTP 或者 RPC。
问题就出现在这里。假设订单服务写库成功,然后调用库存服务扣库存,结果库存服务挂了,或者网络超时了,订单那边已经提交了。你怎么办?
- 如果直接返回失败,用户看到下单失败,但订单表里却躺着一条订单,这就是数据不一致。
- 如果把订单那边也回滚掉,你得让订单服务知道库存服务的结果,并且自己再执行一次“撤销”逻辑,可这个撤销动作本身也可能失败。
- 如果不回滚,靠后台任务对账修复,那用户在看到“下单失败”的同时,库存却被扣了,体验非常差。
服务一拆,原本数据库帮你搞定的事,现在流到了应用层,你不得不自己想办法保证跨服务的“分布式事务一致性”。
1.3 一致性模型:强一致还是最终一致,这是个业务选择题
不少人一听到“分布式事务”,第一反应就是要实现强一致:所有服务要么全部成功、要么全部失败,中间不允许出现任何状态偏差。这个想法本身没问题,但在分布式系统里,强一致是有代价的。
这里绕不开两个经典的定理:CAP定理 和 BASE理论。简单说,CAP告诉你,在网络分区的情况下,可用性和一致性只能选一个。而BASE理论其实就是一种“缓”下来的方案:允许系统在短期内存在中间状态,但保证在一段时间后,数据最终达到一致。
所以你在做技术选型之前,先别急着问“哪个框架最强”,先问自己:业务上到底能容忍多长时间的数据不一致?
举几个例子:
- 转账场景,A扣了100,B必须立刻到账,中间不允许出现“A扣了但B没收到”的状态,这种就需要强一致。
- 电商下单场景,用户下单后,库存从扣减到最终扣减完成,中间差几秒甚至几分钟,用户是无感知的。只要最终订单和库存数据对得上,就是可接受的,这种就适合最终一致。
- 秒杀场景,热点商品库存只有100件,并发可能到几千,这种高并发下如果强行做强一致,锁冲突会直接把系统压垮。
说到底,强一致和最终一致不是“哪个更好”的问题,而是“业务允不允许”的问题。搞清楚这一点,你后面选方案的时候才不会纠结。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四大经典方案逐个拆解:从2PC到事务消息
2.1 两阶段提交(2PC):教科书上的强一致方案,为什么用得少
先讲理论最完美的方案——两阶段提交(2PC)。它把分布式事务分成两个阶段:
- 准备阶段:事务协调者(Coordinator)通知所有参与者(各服务的资源管理器),让他们把本地事务准备好,并持有资源锁,但不提交。
- 提交阶段:协调者收到所有参与者的“准备成功”反馈后,再统一发出“提交”指令;只要有一个参与者准备失败,就统一发出“回滚”指令。
这个思路非常直观,数据库厂商还把它做成了标准接口,也就是XA协议。你写基于XA的全局事务时,应用层其实不用感知太多细节,数据库会帮你协调分支事务。
那问题来了:2PC在真实生产环境中,用得为什么这么少?主要是三个硬伤:
- 同步阻塞:准备阶段所有参与者都要持锁等待协调者的最终指令。这个等待期间,数据库的这行数据是被锁住的,其他正常请求全部卡住,性能损耗非常大。
- 单点风险:协调者如果挂了,所有参与者都不知道该提交还是回滚,只能干等。协调者重启后还得处理一堆“悬而未决”的分布式事务,恢复逻辑非常复杂。
- 数据不一致的窗口依然存在:就算协调者发出了提交指令,如果提交指令在网络上丢了,有的参与者提交了,有的没提交,全局数据依然不一致。
所以2PC更适合那些对强一致极度敏感、并且并发量不大的场景,比如银行核心系统的某些操作。放在互联网高并发业务里,基本都会被性能问题劝退。你在做订单库存这种高并发场景时,我个人的建议是:除非业务强约束必须同步强一致,否则优先考虑下面这三种柔性方案。
2.2 TCC模式:用业务代码换性能,Try、Confirm、Cancel三步走
TCC是我在项目里实际用过比较多的方案。它的核心思想不是依赖数据库事务,而是把“预留资源”和“真正操作”拆开,通过业务代码来实现事务的提交和回滚。
TCC全称是Try-Confirm-Cancel,对应三个阶段:
- **Try
