前言
分布式事务听起来很高大上,其实背后的道理并不复杂。
简单说就是:一个事情要分成好几步做,每一步都可能失败,怎么保证整个事情要么全部完成,要么就像没发生过一样?
为了讲清楚这个问题,我找了6个生活中的场景来打比方。看完你会发现,分布式事务的解决方案,其实我们每天都在用。
一、2PC(两阶段提交):老板发工资的故事
故事场景
你是公司的财务主管,要给全公司100个员工发工资。你担心万一银行系统出问题,有的人发了有的人没发,那就麻烦了。
两阶段操作
第一阶段:提前通知
你先给银行打个电话:"请帮我看一下,如果我现在要给100个员工发工资,你们的系统能处理吗?"
银行检查了一下,回复:"没问题,100个账户都正常,可以处理。"
你把所有员工的钱先"冻结"在公司的账户里,暂时不转出去。
第二阶段:真正发钱
确认所有人都没问题后,你再次给银行打电话:"好了,现在真正转账吧。"
银行把冻结的钱转到每个员工的账户里,发工资完成。
如果中间出问题
假如有个员工的账户被冻结了,银行告诉你"这个发不了",那你就给银行打电话:"全部取消,钱不发了。"然后该干嘛干嘛去。
这个方案的优缺点
优点 :要么全发,要么全不发,很安全。
缺点:中间等待的时间,公司账户里的钱不能动(被冻结了)。如果100个员工里有一个人手机没信号,银行联系不上,那所有人都得等着,谁也别想先拿到钱。
二、3PC(三阶段提交):改进版的发工资
故事场景
还是发工资那个事,但这次公司觉得上次的方案太磨叽,一个人出问题所有人都得等,想改进一下。
三阶段操作
第一阶段:先打电话确认人在不在
你给每个员工打电话:"喂,你手机有信号吗?能接收转账吗?"
每个员工回复:"有信号,没问题。"
第二阶段:通知准备
你给银行打电话:"把100个员工的钱都先冻结,但别转出去。"
银行执行冻结操作。
第三阶段:正式转账
确认冻结成功后,你再次通知银行:"好了,现在转账吧。"
改进的地方
如果银行在第二阶段之后联系不上你(你手机没电了),银行不会一直傻等,而是等一段时间后自动把钱转出去。这样不会因为一个人掉线,所有人干等着。
这个方案的缺点
虽然是改进了,但万一网络断了一下,有的银行节点收到了"转账"指令,有的没收到,还是会出现有人收到钱有人没收到的情况。所以现实中用得不多。
三、TCC:买火车票的"占座"机制
故事场景
你在12306上买火车票,整个过程需要:
-
扣你的钱
-
锁定一个座位
-
生成一张票
如果这三步中间断了,比如钱扣了但座位没锁上,你就很被动。
TCC的三步
第一步:Try(占座但不付款)
你在APP上选好座位,系统提示:"这个座位给你预留30分钟,请尽快付款。"
此时你的银行卡里还没扣钱,只是被"标记"了一下,这个座位别人也不能买了。
第二步:Confirm(确认付款)
你在30分钟内付了款,系统正式把座位分配给你,生成电子票。
第三步:Cancel(取消占座)
如果你30分钟内没付款,或者主动取消,系统自动释放座位,别人可以继续买。
这个方案的优点
整个过程,你的银行卡里的钱并没有真正被扣,只是被"冻结"了一小会儿。即使中间出了问题,也可以随时取消,不会造成实际损失。而且不需要长时间等待。
适用场景
像订机票、订酒店、抢购商品这种,先"占个坑"再付款的流程,天然适合TCC。
四、本地消息表:记账本+闹钟
故事场景
你在网上买了个手机,下单成功后,系统需要给你发一条确认短信。
但是短信服务有时候会挂,如果因为短信发失败导致整个订单取消,那也太不合理了。
解决方案
第一步:先把重要的事记下来
你下单成功后,系统不仅保存了订单,还在自己的小本本(本地消息表)上写了一行:
"2025年1月1日 10:00,需要给用户xxx发一条下单成功的短信,状态:未发送"
第二步:闹钟定时提醒
系统有一个闹钟,每隔5分钟就去翻这个小本本,找出所有"未发送"的短信任务。
第三步:尝试发送
闹钟叫醒系统后,系统尝试去调用短信服务发送短信。发送成功了,就在小本本上把那行记录改成"已发送"。
第四步:失败了就再试
如果短信服务正好坏了,发送失败,那就保持"未发送"状态,5分钟后闹钟再响,再试一次。
这个方案的特点
-
短信服务坏了没关系,订单照样成功,只是短信晚一点发。
-
用本地的小本本(数据库)记录任务,不会丢失。
-
缺点就是闹钟不停地响,对数据库有点压力。
五、事务消息:高级版的"记账本+闹钟"
故事场景
还是买手机发短信的例子,但这次系统不想自己维护那个"小本本"了,太麻烦,想交给专业的MQ消息队列来做。
解决方案
第一步:先发一个"半成品"消息
系统对MQ(消息队列)说:"我要发一条短信消息,但先别发出去,等我通知。"
MQ收到后,把这个消息存起来,但不真正发送。
第二步:完成本地下单
系统继续完成下单操作,把订单存到数据库。
第三步:正式通知MQ发送
下单成功后,系统告诉MQ:"好了,那条消息可以发了。"
MQ这才把短信真正发出去。
第四步:万一系统挂了怎么办
假设系统在第二步和第三步之间突然断电了,订单已经成功,但忘了告诉MQ"可以发了"。
这时候MQ不会傻等,它会主动打电话回查:"喂,你那个订单到底成功了没有?"
系统回答:"成功了,你发吧。"或者"失败了,把那消息删了吧。"
这个方案的优点
-
不需要自己维护消息表,MQ帮你做了。
-
性能更好,吞吐量更高。
-
需要MQ支持"事务消息"功能,目前RocketMQ支持得比较好。
六、Seata AT模式:一个"透明"的魔法助手
故事场景
前面所有方案都需要你改代码、写额外逻辑,很麻烦。
现在有一个神奇的助手(Seata),你什么都不用改,只需说一句"我要全局事务",它就会自动帮你搞定一切。
魔法助手的工作方式
第一步:自动记录
你在代码里写了一个普通的数据库操作:update account set money = money - 100 where id = 1
助手在你执行之前,偷偷把这条记录原来的样子(比如原来有1000块)记录下来,存到一个专门的"撤销日志"表里。
第二步:执行操作
数据库真的把余额改成了900块。
第三步:如果需要回滚
假如后面的操作失败了,助手会自动读取"撤销日志",生成一条反向SQL:update account set money = 1000 where id = 1,然后执行它。
这样你的数据就恢复到了操作前的样子。
为什么说它是"透明"的
因为你在代码里完全看不到任何分布式事务的痕迹,就像在写普通的本地事务一样。助手在背后悄悄地帮你做了所有事情。
这个方案的代价
助手需要做额外的工作(记录前后快照、加全局锁),所以性能会比TCC差一些,但比2PC好。另外,助手本身需要单独部署一个服务。
总结对比表
| 方案 | 生活比喻 | 一句话特点 |
|---|---|---|
| 2PC | 先打电话确认再真正转账 | 安全但慢,一个人卡住全队等 |
| 3PC | 多确认一次以防掉线 | 改进了阻塞问题,但仍有隐患 |
| TCC | 火车票占座30分钟 | 性能好,但需要业务支持"预留" |
| 本地消息表 | 小本本记任务+闹钟提醒 | 简单可靠,但闹钟吵得数据库头疼 |
| 事务消息 | 把记账本交给专业快递 | 性能好,但需要高级MQ支持 |
| Seata AT | 自动记录+自动还原 | 对代码无侵入,最省事 |
最后几句话
-
如果你要绝对安全(比如银行转账),数据不能有半点差错,那就忍受慢一点,用2PC或Seata AT。
-
如果你要高性能(比如秒杀抢购),愿意多写点代码,用TCC。
-
如果你要异步解耦(比如下单后发通知),用事务消息。
-
如果你想最简单,不愿意改太多代码,用Seata AT。
没有最好,只有最合适。
希望这6个故事能帮你把分布式事务的几种方案理清楚。如果哪个比喻还不够准确,或者想深入了解某个方案的技术细节,欢迎继续交流。