财务对不上的那个月
老王去年主导了单体拆微服务:订单、库存、账户各自独立服务,各自独立的数据库。拆完性能漂亮了一个季度,直到月底财务找上门——订单表里有 37 笔已支付订单,库存系统里查无扣减记录。排查溯源,事故当晚库存服务发布抖动,下单链路里订单事务提交成功了,下一跳调库存服务却超时返回,重试被熔断拦截,库存扣减永远没发生。钱收了,货没锁,这批订单差点超卖。单体时代一个 @Transactional 保平安的日子,彻底结束了。
本地事务为什么鞭长莫及
先复习本地事务的护城河:一个数据库连接里的一组 SQL,要么全提交要么全回滚,ACID 由数据库引擎一手包办。它的边界非常清晰——一个连接、一个库。拆完微服务,一次下单变成三个动作:
begin; 订单库 insert 订单; commit; ——订单服务自己的连接
http 调用 库存服务 扣减库存 ——另一个进程另一个库
http 调用 账户服务 扣款 ——又一个进程又一个库三个动作横跨三个进程、三个数据库连接,任何一个本地事务都只能罩住自己那一小段。订单提交和库存扣减之间隔着一次 HTTP 调用,中间的任何时刻进程崩溃、网络抖动、下游超时,系统就停在半路——这不是代码写得差,是物理规律。
分布式世界的三个现实
把责任推给网络之前,先把三个现实钉在墙上:
现实一:网络不可靠。请求可能丢、响应可能丢、丢多久没人知道。你调库存服务「失败」了,可能库存根本没收到,也可能扣完了响应死在路上。
现实二:失败只是一部分。分布式系统没有「全成或全败」,只有 partial failure——三个动作成了两个,系统既不是完成的也不是清空的,卡在一个语义上说不清的中间态。
现实三:超时是三态,不是两态。本地调用结果非成即败;远程调用多出第三种——不知道成没成。超时后重试,可能造成重复扣款;不重试,可能漏扣。所有分布式事务方案,本质上都在回答同一个问题:第三态怎么办。
直觉解法为什么撑不住
事故后老王的第一版修复很直觉:try-catch 包住三个调用,谁失败就把前面成功的逐个调回去。两周后代码长成这样:订单回滚接口、库存回滚接口、账户回滚接口,回滚链里任何一跳再失败,又得为「回滚的回滚」写逻辑——补偿代码的复杂度指数膨胀,且永远追不上故障组合的数量。更别提回滚接口自己被调了两次怎么办(幂等)、回滚时原服务已经恢复了怎么办(悬挂)——这些坑,后面专篇讲。
直觉解法输在散兵游勇:没有统一的协议管「谁先谁后、失败了谁负责救、救失败怎么办」。业界花了几十年沉淀的,就是把补偿从「各写各的」升级成「有章法的模式」。
方案的谱系:从强一致到最终一致
把市面上的方案按「一致性强度」排成一条谱系,整个系列就住在这条线上:
| 阵营 | 代表方案 | 核心思路 | 代价 |
|---|---|---|---|
| 强一致 | 2PC / XA / 3PC | 先表决再提交,全员点头才生效 | 同步阻塞、可用性差 |
| 补偿型 | TCC / Saga | 先干,不对就按业务逻辑逆操作 | 业务改造重、写补偿 |
| 消息型 | 本地消息表 / 事务消息 | 用可靠消息串起最终一致 | 中间态可见、依赖 MQ |
| 兜底型 | 对账 / 最大努力通知 | 承认会漏,定期找补 | 有时间窗口、人工介入 |
谱系往左,一致性越强、可用性越差;往右,可用性越好、中间态越明显。老王后来的库存问题最终落在「消息型 + 对账兜底」的组合上——这个选择的推理过程,就是接下来十七篇的内容。
本系列的行军地图
十八篇分四段:第 2 篇打底理论(ACID、CAP、BASE);第 3 到 10 篇逐个拆方案——2PC、TCC 及其三大坑、Saga、本地消息表、事务消息、最大努力通知、幂等设计;第 11 到 15 篇进入框架与实战——Seata 四模式选型、AT 模式原理、热点账户、超时重试、事务边界七宗罪;第 16 到 18 篇收尾——对账系统、踩坑实录、收官地图。
小结
开篇一句话:拆微服务拆掉了本地事务的护城河,网络不可靠、部分失败、超时三态是绕不开的物理现实;分布式事务不是让分布式变回单体,而是用一套套模式管理中间态。先别急着看方案——下一篇把理论地基打平:ACID、CAP、BASE 到底在说什么,以及为什么 BASE 才是互联网架构的宿命。
评论 (0)