分布式事务几种常见解法

聊聊分布式事务是怎么冒出来的,2PC、TCC、本地消息表、Saga这几种常见方案各自的原理、优缺点和适用场景,配着实际业务场景举例讲,帮你理清什么场景该选哪种方案。

分布式事务几种常见解法

前段时间自己捣鼓一个小项目,把原来的单体服务拆成了订单服务和库存服务两个独立部署的模块。

拆完之后马上就撞到一个问题:下单的时候要同时扣库存和建订单,这两个操作现在分别在两个数据库里,单机事务的 COMMIT/ROLLBACK 直接失效了。

这就是分布式事务要解决的核心问题,今天我们把常见的几种解法捋一遍,顺带搞懂它们各自在拿什么换什么。

分布式事务是怎么冒出来的

我们先回忆一下单机事务:一个数据库连接里执行多条 SQL,要么全成功要么全失败,靠数据库自己的事务机制保证。

一旦业务拆成多个服务、多个数据库(甚至可能是不同类型的数据库,比如订单服务用 MySQL、库存服务用 MongoDB),原来的单机事务就管不到了。

打个比方,单机事务像是一个人同时管两件事,做错了直接自己全部撤销。

分布式场景下变成了两个人分别负责两件事,各自只能操作自己手头的东西,谁也不能直接控制对方,怎么保证两人的动作要么一起成功要么一起撤销,这就是分布式事务要解决的协调问题。

业界的思路大体分两类:强一致性方案(尽量让结果看起来跟单机事务一样,操作过程中会有短暂的资源锁定)和最终一致性方案(先接受短暂的数据不一致,靠后续补偿或重试让数据最终达成一致)。

2PC 两阶段提交

原理

2PC(Two-Phase Commit)是最经典的强一致性方案,引入一个统一的协调者(Coordinator),把提交过程拆成两个阶段。

第一阶段(准备阶段):协调者问所有参与者“你这边准备好提交了吗”,每个参与者执行操作但不真正提交,锁住相关资源,回复“准备好了”或者“不行”。

第二阶段(提交阶段):如果所有参与者都回复“准备好了”,协调者通知大家正式提交;只要有一个参与者说“不行”,协调者就通知所有人回滚。

举个例子:订单服务和库存服务都接入同一个事务协调器(比如支持 XA 协议的数据库中间件),下单时协调者先问两边“扣库存、建订单都能做吗”,都说能做,再统一喊“提交”。

优缺点

优点很直接:能做到真正的强一致性,效果跟单机事务基本一致,业务代码也相对简单,不需要自己写补偿逻辑。

缺点也很致命:

  • 同步阻塞:准备阶段所有参与者的资源都被锁住,等待协调者的最终指令,如果有一个参与者很慢,其他人都得干等着,吞吐量上不去。
  • 协调者单点风险:如果协调者在发出提交指令之后自己挂了,参与者会一直卡在“不知道到底提交没提交”的状态,这个问题叫“协调者故障”,需要额外的容错机制来兜底。
  • 性能开销大:多了一轮网络交互,加上资源锁定的时间成本,在高并发场景下性能压力明显。

所以 2PC 比较适合对一致性要求极高、并发量不算特别夸张的场景,比如银行间的跨行转账。互联网高并发业务用得相对少。

TCC 手动补偿模式

原理

TCC(Try-Confirm-Cancel)不依赖数据库层面的事务机制,而是让业务代码自己实现三个阶段的接口:

  • Try:尝试执行,预留资源(不是真正扣减,而是先“冻结”)。
  • Confirm:所有参与者 Try 都成功后,真正确认执行,把冻结的资源正式扣掉。
  • Cancel:只要有一个参与者 Try 失败,通知所有人取消,把冻结的资源释放回去。

拿库存的例子讲:Try 阶段不是直接把库存数字减掉,而是把这部分库存标记成“冻结中”,别人下单看不到这批库存但也没真正扣掉;Confirm 阶段才是真正把冻结的库存扣除;Cancel 阶段就是把冻结状态解除,库存恢复可售。

优缺点

优点是性能比 2PC 好很多,因为资源不是死锁住等待,而是业务层面的“预留”状态,不会长时间阻塞数据库连接。

缺点是业务侵入性强:每个参与业务都要自己实现 Try/Confirm/Cancel 三套接口,开发和维护成本明显增加,而且需要额外处理“空回滚”“悬挂”这类边界情况(比如 Cancel 请求比 Try 请求先到,或者 Try 根本没执行成功但 Cancel 却被调用)。

TCC 比较适合对性能要求较高、同时又需要相对强一致性保证的核心链路,比如电商大促下单、支付这类场景,很多互联网公司的分布式事务框架(如 Seata)都提供了 TCC 模式支持。

本地消息表方案

原理

这是一种最终一致性方案,核心思路是把“业务操作”和“消息发送”绑定在同一个本地事务里。

流程大致是这样:订单服务在自己的数据库里,用同一个本地事务同时写入订单记录和一条“待发送”状态的消息记录;本地事务提交成功后,有个后台任务扫描这张消息表,把状态为“待发送”的消息真正发到消息队列(如 RocketMQ、Kafka);库存服务订阅这个消息,收到后执行扣库存操作,处理成功再确认消费。

因为订单写入和消息记录写入是同一个本地事务,要么都成功要么都失败,不会出现“订单建了但消息没发出去”的情况,消息队列本身也有重试机制,保证消息最终能送达。

优缺点

优点是实现相对简单,不需要专门的分布式事务协调器,纯粹靠本地事务加消息队列的组合就能搞定,性能也不错,属于异步处理,不会长时间锁资源。

缺点是只能做到最终一致性,不是实时一致:库存服务可能过一小段时间才处理完消息,这段时间查询到的数据是不一致的(订单已经建了,但库存还没扣)。而且需要一个额外的表和扫描任务,还要处理消息重复消费的问题(消费端要做好幂等处理)。

这种方案在互联网业务里用得非常多,比如下单成功后异步通知积分服务加积分、通知短信服务发提醒,这类不要求实时强一致、但要求“最终必须做到”的场景特别合适。

Saga 分段补偿模式

原理

Saga 模式把一个长事务拆成一系列本地事务步骤,每个步骤都对应一个补偿操作。如果中间某一步失败,就按顺序反向执行前面已完成步骤的补偿操作,把状态“撤回去”。

举个例子,一次旅游订单可能涉及订机票、订酒店、订门票三个步骤。如果订门票失败了,Saga 会依次调用“取消酒店”“取消机票”这两个补偿动作,把前面已经做的操作撤销掉,而不是像 2PC 那样提前锁定资源。

优缺点

优点是不需要像 2PC 那样锁资源,也不需要 TCC 的“预留”阶段,每一步都是直接执行的本地事务,性能好,也比较适合步骤多、链路长的业务流程。

缺点是补偿逻辑设计起来比 TCC 还复杂一些:步骤越多,中间状态越多,需要仔细设计每一步对应的补偿动作,而且补偿过程本身也可能失败,需要额外兜底(比如人工介入或者重试队列)。另外 Saga 整个过程是可能被外部观察到中间状态的(比如机票订好了但酒店还没订好这个瞬间是可查询到的),不是隔离的。

Saga 比较适合业务流程本身就是多步骤、长链路的场景,比如旅游预订、供应链下单这类涉及多个子系统协作的业务。

挑一个的时候怎么想

简单归纳一下几种方案的取舍方向:

  • 如果业务对一致性要求极高、可以接受一定的性能损耗:2PC。
  • 如果核心链路需要性能和一致性都兼顾,愿意承担业务侵入的开发成本:TCC。
  • 如果能接受最终一致性,业务是“下单后触发一连串异步动作”这种模式:本地消息表。
  • 如果业务流程本身是多步骤长链路,且步骤之间有清晰的补偿动作:Saga。

没有哪个方案是全能的,实际项目里 even 经常是组合使用:核心支付链路上 TCC,外围通知类操作用本地消息表,具体怎么选还是要看业务对一致性和性能的真实需求。

小结

分布式事务说到底是在一致性、性能和开发成本这三者之间找平衡点。

2PC 换来强一致性但牺牲了并发性能;TCC 换来了性能但要多写补偿代码;本地消息表和 Saga 都是接受最终一致性,用异步和补偿的思路换来更好的吞吐量。

没有绝对的好坏,关键是搞清楚自己的业务到底能不能接受“中间有一段时间数据不一致”,接受不了就往强一致性方案靠,能接受就大胆用最终一致性换性能。

PS:不知道为什么,很多资料讲这几种方案的时候都是孤立展开,没有放在一起对比取舍逻辑,希望这篇能帮你把这几个方案在脑子里串成一条线。

那分布式事务这块就先讲到这里,日常做系统设计的时候,建议先问自己一句“这个场景能不能接受最终一致”,答案会帮你排除掉一半的选项。

更多推荐

章节目录