ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分布式事务理论与模型选型:从CAP到Seata

分布式事务理论与模型选型:从CAP到Seata 第一次在线上排查分布式事务问题我记得很清楚。那是一个周五晚上运营反馈说后台能看到用户下单成功了但订单里有一批商品的库存没有扣减财务对账也对不上。我第一反应是某个服务接口报错了准备去日志里翻异常结果翻遍了订单服务和库存服务的日志没发现任何报错——所有调用都返回了成功。那一瞬间我意识到问题已经不是某个接口的Bug而是跨服务的数据一致性问题。也就是从那时候开始我才系统地接触分布式事务这个概念去啃CAP、BASE、2PC、TCC、SAGA这一大堆理论模型。这篇文章就是我梳理分布式事务概念和理论模型的完整笔记。不打算讲具体的源码也不绑定某一种中间件而是把这块知识体系从头捋一遍分布式事务到底解决什么问题理论模型有哪些强一致和最终一致分别怎么实现落到真实业务里怎么选。适合刚接触微服务、准备做系统拆分的开发同学也适合那些已经在用Seata之类的框架但一直没搞懂底层原理的人。1. 先从订单和库存说起分布式事务到底在解决什么问题1.1 单体应用里的一次下单有多简单在聊分布式事务之前得先搞清楚它的对立面——单体应用里的本地事务。传统的单体应用用户表、订单表、库存表都在同一个数据库里。用户下单这个操作本质上是往订单表插入一条记录同时更新库存表的库存数量如果涉及积分再更新一下用户积分。整个流程可以被一个数据库事务包住插入订单记录扣减库存数量更新用户积分三个操作要么全部成功要么全部失败。数据库的ACID特性保证了这一点原子性保证三个操作作为一个整体不可分割隔离性保证并发事务之间互相不干扰持久性保证事务一旦提交数据不会丢。开发时只要在方法上加上事务注解数据库自己就把这件事处理得明明白白。单体架构下事务边界和业务边界天然重合因为数据都在一起。这也是为什么早期很多业务系统开发起来感觉很简单——复杂的一致性处理被数据库屏蔽掉了。1.2 微服务拆开后一个下单请求经历了什么后来业务规模变大团队变多单体应用撑不住了开始做微服务拆分。订单、库存、积分分别拆成独立的服务每个服务有自己的数据库。这是微服务架构的标准姿势服务自治、数据库隔离。但问题立刻就来了。用户再次下单时调用的链路变成了这样订单服务在自己的库里插入订单记录订单服务通过RPC调用库存服务扣减库存订单服务再通过RPC调用积分服务给用户加积分如果每一步都成功一切看起来和以前没区别。但分布式环境里部分失败是常态不是异常。库存服务扣库存成功了但积分服务调用超时订单服务这边直接抛异常回滚了——用户看到的是下单失败但库存却莫名其妙少了。反过来也一样下单成功后扣库存的RPC调用超时了订单服务重试发现也连不上库存服务于是订单回滚但库存其实已经扣了。这就是我开头说的那个线上事故的本质没有哪个服务报错每个服务内部的本地事务都执行成功了但跨服务的数据状态对不上。1.3 核心矛盾本地事务的手伸不到别人的数据库这个问题为什么这么难解因为分布式事务的核心矛盾在于本地事务的边界永远无法覆盖跨服务的操作。一个数据库事务只能控制自己库里的数据。订单服务的事务管不到库存服务的数据库库存服务的事务也管不到订单服务的数据库。如果两个服务各自开一个本地事务那么这两个事务之间没有任何关联协调不了。有人会说那我把RPC调用放到本地事务中间不就行了——先插入订单再调用库存服务扣库存最后提交本地事务。但这样做的结果是如果本地事务最终决定回滚库存那边已经扣减成功没法跟着一起回滚。或者本地事务提交之后库存服务的调用还没发出系统就宕机了订单有了库存却没扣。要解决这个问题就需要一个跨服务的协调机制让多个服务各自独立的本地事务像一个整体一样协同工作。这个机制就是分布式事务。分布式事务要保证的是跨多个服务、多个数据库资源的数据一致性。而设计这样一套机制首先要想清楚一个底层问题在分布式环境下一致性到底意味着什么这就引出了分布式事务最基础、也最重要的两个理论基石——CAP定理和BASE思想。2. 理论地基CAP定理与BASE思想的取舍逻辑2.1 CAP说的三选二为什么实际是P必选学习分布式事务绕不开CAP定理。这个定理是计算机科学家Eric Brewer在2000年提出的核心观点是一个分布式系统在一致性Consistency、可用性Availability、分区容错性Partition tolerance这三个特性中最多只能同时满足两个。这三个特性的定义值得多说几句因为它们经常被误解。一致性指的是所有节点在同一时刻看到的数据是相同的。用户往A节点写入了一个值立刻去读B节点必须读到这个新值。如果B节点上还是旧值那就说明不一致。可用性指的是系统对请求必须给出响应而且这个响应不能是错误。只要系统还在运行每个请求都应该在合理时间内收到正常的反馈。分区容错性指的是系统在发生网络分区节点之间无法通信时依然能继续工作。网络分区在分布式系统里不是极端情况而是常态——机房光纤被挖断、网络抖动、机器宕机、消息丢失都会导致节点间的通信中断。这里有个关键点P不是你想不想选的问题而是必须面对的现实。只要你的系统是分布式的网络分区就一定会发生。所以CAP在实际应用中的含义不是三选二随便挑而是在P必选的前提下在C和A之间做取舍。一旦网络分区发生节点之间断开了联系系统只能二选一保一致性CP为了确保所有节点数据相同系统拒绝那些无法确认是否一致的请求宁可暂时不可用。比如某些配置中心、注册中心走的是这条路线。保可用性AP为了保证系统持续响应节点各自处理请求允许出现暂时的数据不一致等到网络恢复后再慢慢同步。大部分互联网业务系统走的是这条路线。那这和分布式事务有什么关系关系非常大。分布式事务本质上就是试图在分布式环境下把数据从不一致的状态收敛到一致的状态。CAP定理告诉我们如果你想保证强一致就必须牺牲一定的可用性如果你要保可用性就得接受数据在一段时间内可能是不一致的。一切分布式事务模型都是在这两个方向之间找平衡点。2.2 从ACID到BASE事务语义的降级不是妥协CAP定理给分布式事务开了两道门一道通向强一致另一道通向最终一致。BASE思想就是通向最终一致的那道门。BASE是三个词的缩写Basically Available基本可用。系统在出现故障时允许损失部分可用性比如响应时间变长、部分功能降级但核心功能仍然可用。Soft state软状态。允许系统在不同节点之间存在临时性的数据不一致状态这个状态可以随着时间推移自动或被动地改变。Eventually consistent最终一致。系统经过一段时间的自愈或补偿操作数据最终会达到一致状态。BASE本质上是把ACID里的一些强约束放松了。ACID要求强一致性BASE只要求最终一致ACID要求立即一致BASE允许暂时不一致ACID的隔离性是各事务互不可见BASE的软状态则允许中间状态被观察到。我见过不少刚从单体转微服务的团队看到BASE的第一反应是这不就是放弃一致性了吗。这种理解是有偏差的。BASE不是放弃一致性而是把一致性的保证从数据库级转移到了应用级。也就是说数据库不再帮你保证跨服务的强一致应用需要自己设计一套异步补偿、消息重试、对账机制让数据最终能收敛到一致。从ACID到BASE不是变弱了而是把一致性的责任从基础设施上浮到了业务逻辑里。2.3 一致性程度的光谱强一致、弱一致与最终一致实际工程里一致性不是非黑即白而是一条光谱。理解这条光谱对后面选择具体的事务模型很有帮助。强一致任何时刻任何节点读到的数据都是最新的。用户写入后立刻读必然读到刚写入的值。CAP里的C就是这种级别。单体数据库的本地事务天然提供强一致。弱一致系统不保证写入后立即可见只保证后续某个时间点会可见但不承诺具体时间。DNS就是一个典型的弱一致例子你修改了域名解析记录可能需要几分钟甚至更久才能在全球生效。最终一致弱一致的一个特例。它承诺只要系统没有新的写入经过一段未知的时间后所有副本最终会达到一致。BASE里的E就是它。分布式事务领域里强一致方案的典型代表是两阶段提交、三阶段提交最终一致方案的典型代表是TCC、SAGA、本地消息表、事务消息。注意这里有个很多人容易搞错的概念TCC虽然叫补偿型事务但它其实是趋近于强一致的最终一致——它用加锁、预留资源的思路尽量压缩不一致的时间窗口但本质上还是要靠业务补偿来兜底。有了这条光谱我们看各种分布式事务模型时就会有一个清晰的坐标系每个模型都在强一致和最终一致之间占据一个位置位置越靠近强一致通常对可用性的牺牲就越大越靠近最终一致对业务补偿能力的要求就越高。接下来就沿着这个坐标系从强一致一侧开始逐个拆解主流的分布式事务理论模型。3. 强一致路线2PC与3PC的机制、代价与局限性3.1 两阶段提交的运行流程准备阶段与提交阶段两阶段提交Two-Phase Commit2PC是最经典的强一致分布式事务模型几乎所有关于分布式事务的讨论都会从它开始。很多分布式数据库和消息中间件在底层都借鉴了它的思想。2PC里有个核心角色叫协调者Coordinator通常由事务管理器或应用服务器充当。其他参与事务的节点叫参与者Participant通常是各个数据库资源。2PC把一次分布式事务分成两个阶段第一阶段准备阶段Prepare Phase。协调者向所有参与者发送准备提交的请求。每个参与者收到请求后执行本地事务操作但不提交——资源已经锁定undo日志和redo日志都写好了然后把执行结果成功或失败报告给协调者。这个阶段参与者做的操作可以理解为万事俱备只欠东风事务所需的所有条件都满足了但数据还没有真正对外生效。第二阶段提交阶段Commit Phase。协调者收集所有参与者的准备结果。如果所有参与者都返回成功协调者就向所有参与者发送正式提交命令参与者把本地事务真正提交释放资源。只要任何一个参与者返回失败或者超时没有响应协调者就向所有参与者发送回滚命令大家把第一阶段锁定的资源释放撤销未提交的操作。这个流程看起来严谨但它能正确工作的前提是网络是可靠的协调者不会挂掉参与者不会在提交过程中宕机。显然分布式环境下这三条一条都保证不了。所以2PC在实际工程落地时暴露出了一堆问题。3.2 2PC的三大痛点同步阻塞、单点故障与脑裂2PC的第一个痛点是同步阻塞。在准备阶段参与者需要锁定自己的资源这些资源在协调者发出最终指令前一直处于被占用的状态。如果某个参与者响应慢或者协调者迟迟不给结论其他事务访问这些资源时只能等待。数据库层面的表现就是行锁、表锁长时间不释放事务响应时间拉长并发能力急剧下降。第二个痛点是协调者单点故障。协调者是整个2PC流程的中枢一旦它在发送准备请求之后、发送提交/回滚命令之前宕机了所有参与者都在傻傻地等待最终指令资源锁无法释放事务陷入无限期的阻塞状态。更麻烦的是协调者恢复后它的事务状态日志如果不够完整可能连自己都搞不清之前到底该提交还是该回滚。第三个痛点是脑裂导致的数据不一致。第二阶段里协调者向所有参与者发送提交命令。假设它发送到了前两个参与者第三个参与者所在网络突然断开了提交命令没送达。前两个参与者提交了数据第三个参与者还在那等指令最后超时也没等到只能执行超时逻辑。这就造成同一个分布式事务里一部分参与者提交了另一部分没有数据一致性已经被破坏。这三个痛点决定了2PC只适合那些对强一致要求极高、并发量不大、参与者数量少且运行稳定的场景。现实中直接裸用2PC的业务系统其实不多但它的思想被广泛吸收比如后面要提到的Seata的AT模式底层就有2PC的影子。3.3 3PC的改进思路与仍然存在的问题三阶段提交Three-Phase Commit3PC是2PC的改进版主要针对2PC的阻塞问题做了两点调整一是把准备阶段进一步拆分成CanCommit和PreCommit两个子阶段增加了一个预处理步骤二是给参与者也引入了超时机制参与者不再无限期等待协调者的指令。3PC的三个阶段是这样的CanCommit阶段协调者向参与者询问你们能不能提交这个事务参与者只需评估自身状态、给出可以或不可以的答复不执行任何实际操作也不锁资源。这一步相当于一次预检。PreCommit阶段如果所有参与者都答复可以协调者发送预提交请求各参与者执行本地事务操作写日志、锁资源然后返回预提交成功。这个阶段对应2PC的Prepare阶段。DoCommit阶段协调者收到所有参与者的预提交成功回复后发送正式提交命令。参与者收到命令后真正提交事务并释放资源。3PC最大的改进是如果进入DoCommit阶段后参与者迟迟没有收到正式提交的命令它不会一直死等而是会在超时后自动提交本地事务。为什么可以这样设计因为能够走到DoCommit阶段说明前面的CanCommit和PreCommit都成功了提交是大概率正确的结果。但3PC并没有从根本上解决2PC的问题。它只是减少了阻塞的时间窗口把无限期阻塞变成了超时后的自动提交。如果网络分区发生在DoCommit阶段一部分参与者收到提交命令提交了另一部分参与者因为超时也自动提交了看起来问题不大——但如果协调者在PreCommit阶段之后因为超时判定某个参与者异常决定整个事务要回滚而其他参与者却在超时后自动提交了数据不一致还是会出现。所以3PC在理论上比2PC优雅在实际工程中的应用却更少——它的复杂度和收益完全不成正比。关于强一致路线我的结论是2PC和3PC的模型价值在于给我们提供了一套关于协调、预操作、提交决策的思维框架但直接作为业务系统的分布式事务方案代价太大。绝大多数互联网业务选择的都是另一条路——柔性事务也就是最终一致路线。4. 柔性事务的主流理论模型TCC、SAGA、本地消息表与事务消息4.1 TCC把事务动作拆成Try、Confirm、Cancel三段TCCTry-Confirm-Cancel是我个人在实际项目里用得最多、也最推荐花时间吃透的柔性事务模型。它的核心思想是把业务逻辑拆成三个方法。Try阶段完成资源检查和预留。这个阶段不真正执行业务操作而是占坑。比如扣库存业务Try阶段做的就是冻结库存——在库存表里把200件商品的100件标记为冻结状态而不是直接扣减。Confirm阶段提交执行。如果所有参与者的Try都成功了就进入Confirm阶段把预留的资源真正扣减掉把冻结的库存改成已售出。这个阶段的操作是不会失败的因为安全性在Try阶段已经确认过了。Cancel阶段回滚释放。如果任何一个参与者的Try失败或者事务整体需要回滚就执行Cancel操作把Try阶段冻结的资源释放回可用池。拿最经典的下单扣库存举例。订单服务执行Try创建一条状态为待确认的订单库存服务执行Try把100件库存冻结。全部Try成功后进入Confirm订单状态改成已支付库存从冻结改成已扣减。如果某个Try失败所有参与者执行Cancel订单状态改成已取消库存解冻。TCC的最大价值在于它把锁定资源和执行操作解耦了——Try阶段持有资源但不操作Confirm阶段操作但不加锁。这样既避免了2PC那种长时间锁库的问题又比纯粹的事后补偿更安全因为资源从Try开始就被保护起来了。4.2 TCC落地时的两个经典坑空回滚与悬挂TCC听起来思路简单但落地时有两个非常隐蔽的坑我当初在这上面栽过跟头。第一个坑是空回滚。一个TCC事务里如果某个参与者的Try方法因为网络超时没有执行成功但事务协调器已经决定回滚就会直接调用这个参与者的Cancel方法。此时Cancel发现Try根本没有执行过就没有什么可回滚的资源。如果Cancel不做非空判断直接按正常回滚的逻辑去释放资源就可能出现释放了不属于本次事务的资源的情况。解决方法是在Try和Cancel里都记录事务状态Cancel执行前先查询Try是否执行过没有执行就不做任何操作。第二个坑是悬挂。和空回滚相反Try方法因为网络超时没有返回结果协调器判定失败并执行了CancelCancel成功执行了。但此时那个迟到的Try请求才到达参与者资源被冻结了可已经没有后续的Confirm或Cancel会来释放它——这些资源就永久悬挂在那里了。解决悬挂问题的核心是作废掉迟到的Try请求通常要借助分布式锁或唯一事务标识让Try在执行前先判断事务是否已经被Cancel过。TCC的三个方法必须由业务系统自己实现代码侵入性很强但它换来的是对业务资源的精确控制。它适合那些需要强约束、不允许超卖、不允许资损的场景——比如库存预占、账户扣款、余额变动。4.3 SAGA长事务的另一种补偿思路SAGA模型最早来自数据库领域1987年由Saga等人提出后来被应用在微服务的分布式事务中。它的思路和TCC完全不同不做资源预留直接执行正反向两套操作。SAGA把一个长事务拆成一组有序的本地子事务每个子事务都有自己的正向操作以及对应的反向补偿操作正向执行订单创建成功 - 库存扣减成功 - 用户扣款成功补偿执行如果用户扣款失败就依次执行扣款补偿退款、库存补偿加回库存、订单补偿取消订单也就是说SAGA不锁定任何资源而是在正向执行过程中通过保存执行流程的轨迹在出错时用反向操作把已经完成的效果撤销掉。SAGA有两种实现方式。一种是编排式Choreography每个服务完成后通过事件或消息通知下一个服务没有中央协调者。优点是去中心化缺点是流程逻辑分散在各个服务里出错时排查链路比较费劲。另一种是协同式Orchestration用一个中央协调器来编排所有的正向和反向调用流程逻辑集中方便管控和排查。SAGA相比TCC最大的优势是简单——业务方只需要实现正向操作和补偿操作即可不需要预留资源也没有空回滚、悬挂这些复杂问题。但它也有个明显的短板没有隔离性。在SAGA执行过程中其他事务可能读到中间状态的数据。比如SAGA先创建了订单、还没扣款订单系统就开始发货了最后整个事务回滚取消订单发货操作却可能已经触发了。所以使用SAGA的业务场景要能容忍中间状态的可见性或者通过业务规则来兜底。4.4 本地消息表最朴素的最终一致性方案如果说TCC和SAGA都还带着协调的色彩那本地消息表就是一种完全放弃协调、靠异步重试来保证最终一致的模型。它是国内互联网行业早期用得最多的分布式事务方案思想最早可以追溯到eBay的工程师在2008年分享的文章。本地消息表的核心设计非常巧妙把发消息这个操作和业务操作绑定在同一个本地事务里。还是拿下单举例。订单服务要完成两件事写入订单记录通知库存服务扣库存。传统做法是写入订单后立刻发RPC通知库存服务这就有RPC失败的风险。本地消息表的做法是订单服务在自己的数据库里开启一个本地事务事务内插入订单记录同时往一张消息表里插入一条待发送状态的消息记录的内容是订单ID、商品ID、扣减数量、状态提交这个本地事务注意这两件事在同一个数据库事务里要么都成功要么都失败。所以只要订单记录存在消息记录就一定存在。这一步就保证了业务操作和消息发送的原子性。接下来后台有一个定时任务或消息发送程序每隔一段时间扫描消息表把状态为待发送的消息取出来投递给库存服务的消息队列或RPC接口。投递失败就继续重试直到成功为止。库存服务收到消息后执行自己的扣减逻辑并且要保证幂等——同一个消息被重复投递时不会重复扣减库存。本地消息表这个模型没有引入任何外部中间件只靠一张数据库表和定时任务就能保证分布式环境下数据的最终一致。它最大的优点是可靠——业务操作和消息生成的原子性由数据库事务保证消息投递失败可以无限重试。缺点也明显消息表耦合在业务库里会占用业务库的存储和IO而且需要自己维护定时扫描、重试、去重的逻辑。4.5 事务消息把消息表下沉到消息中间件本地消息表出现后大家发现一个更优雅的演进方向把消息表定时任务这套东西下沉到消息中间件里让中间件来保证消息的可靠性。这诞生了事务消息模型RocketMQ是其中最著名的实现。RocketMQ的事务消息机制分为两个阶段发送半消息Half Message生产者把消息发送到MQ此时消息对消费者是不可见的处于一个半就绪状态。执行本地事务MQ收到半消息后回调生产者本地事务接口生产者执行真正的业务逻辑比如插入订单记录。执行完成后生产者向MQ提交事务状态如果本地事务成功就告诉MQ这条消息可以发出去MQ把半消息变成可消费的消息如果本地事务失败就告诉MQ这条消息作废MQ直接丢弃。这里有个关键问题如果业务方执行完本地事务后在向MQ提交事务状态时崩了MQ拿不到提交结果怎么办此时MQ会启动事务回查机制——定时向生产者询问这条半消息对应的本地事务到底提交了没。生产者需要实现一个反查接口去查本地的订单记录是否存在存在就回复提交成功不存在就回复提交失败或未知。事务消息模型本质上是本地消息表的中间件版它的优势是业务代码不用维护消息表和定时任务消息的可靠性和投递语义由MQ保证消息表不会占用业务库的资源回查机制让最终一致的收敛过程更自动化。目前RocketMQ和Pulsar都支持事务消息Kafka其实也有类似实现但用得不多。到这里主流理论模型已经全部出场了。很多人学到这就会迷茫模型这么多到底该用哪个接下来我给出我自己的选型思路和判断标准。5. 模型对比与选型订单库存这种场景到底该怎么选5.1 六种模型的边界对比先把刚才讲过的模型放在一张表里做个横向对比这样大家能直观看到它们的差异。模型一致性方向代码侵入隔离性性能影响典型场景2PC强一致低数据库层/ 依赖协调器强资源锁很差同步阻塞跨库强一致小众、低并发3PC趋近强一致低一般较差理论模型多实际部署少TCC最终一致接近强高3个方法可自定义冻结资源中库存预占、账户扣款、资金类SAGA最终一致中正反向操作弱无隔离较好长流程、跨服务编排、容忍中间态本地消息表最终一致中消息表和定时任务弱中业务库压力消息通知、业务与消息原子性事务消息最终一致低依赖MQ弱较好MQ分担异步解耦、订单创建、积分赠送注意表里的代码侵入和一致性方向只是相对判断。比如2PC在数据库层面实现时业务代码几乎不用改但它的性能代价巨大TCC代码改动量大但换来的是比较高的可控性。5.2 选型判断的三个关键维度现实中选择分布式事务模型我一般只看三个维度一致性要求、性能要求、团队改造能力。第一个维度资金和库存这类强约束场景优先TCC。以订单和库存为例库存超卖是不可接受的。如果我用纯异步的事务消息消息在MQ里积压或重试期间库存还没有扣减此时用户再次下单系统看到的是旧库存——虽然最终会一致但在那段时间窗口里超卖风险是实实在在的。而TCC的Try阶段就把库存冻结了从源头上杜绝了超卖。资金扣款同理TCC的冻结-确认两步操作能精确控制资金状态。第二个维度性能优先、容忍短暂不一致优先事务消息或SAGA。比如用户下单成功后要发短信通知、加购物车积分、更新用户推荐标签。这些操作晚几分钟执行用户根本感知不到但它们如果阻塞在主流程里下单接口的响应时间会明显上升。这时候用事务消息把写订单和发消息绑成一个事务消息异步消费响应快、解耦好。第三个维度团队的技术能力和运维成本。这是最容易被忽略的。TCC要业务方实现三个方法还要处理空回滚、悬挂对开发人员的理解要求很高。SAGA的编排逻辑需要维护。本地消息表虽然简单但要自己写定时任务和重试逻辑。如果团队没有对应的技术储备我会建议优先选成熟的中间件方案比如用RocketMQ的事务消息、或者引入Seata这样的分布式事务框架而不是从零手写一套TCC编排器。5.3 没有银弹混用才是常态还有一点很重要一个复杂的业务链路里往往不是只用一种模型。我见过不少团队非要在整个下单链路里统一用一种方案结果越弄越别扭。实际上分布式事务的选型应该是分段治理的。在下单链路里我通常是这么分的订单创建 库存预占TCC因为库存明确不能超卖需要强约束订单支付回调 积分赠送事务消息积分晚到一会儿没关系但需要保证最终到账用户取消订单 库存回补SAGA订单、库存、退款三个服务走编排式补偿这样设计的好处是每个环节用自己的模型保证自己的局部一致性整个链路通过事件或消息串联起来。分布式事务不追求全局一把锁而是让每个环节的最终一致衔接成一条完整的数据一致性链路。理论模型选型的问题解决了但很多开发同学还有一个困惑这些模型理论离实际开发有点远尤其是遇到生产问题的时候理论真的能指导排查吗我拿Seata这个主流框架来具体说一下理论和框架是怎么对上的。6. 从理论到落地以Seata为例看分布式事务框架的实现思路6.1 Seata的事务角色划分TC、TM、RMSeata是阿里开源的一套分布式事务解决方案目前在Java生态里用得相当广。它提出了三个核心角色理解了这三个角色也就理解了所有协调型分布式事务框架的骨架。TCTransaction Coordinator事务协调器。一个独立部署的服务负责全局事务的注册、分支事务的注册、全局事务的提交/回滚调度。对应2PC模型里的协调者。TMTransaction Manager事务管理器。嵌入在业务应用里负责开启全局事务、提交全局事务、回滚全局事务。简单说TM就是业务代码里那个总入口它告诉TC我要开始一个全局事务了我要结束这个全局事务。RMResource Manager资源管理器。同样嵌入在业务应用里负责管理分支事务。业务应用每执行一个本地事务RM就向TC注册一个分支事务并报告执行结果。对应2PC模型里的参与者。一次标准的下单操作在Seata里的流程是这样的TM向TC发起全局事务拿到一个全局事务ID然后这个ID会通过调用链一路传递下去——订单服务、库存服务、积分服务都带着同一个ID。每个服务的本地事务执行时本地的RM会向TC注册分支事务。所有业务操作完成后TM向TC发起全局提交TC检查所有分支事务的状态决定整体提交或整体回滚。这个流程你看其实就是2PC的分布式升级版本地事务负责各自的数据操作TC负责全局决策TM负责业务边界。区别在于Seata里的本地事务是直接提交的业务操作不用锁资源等指令所以在性能上比裸用2PC好很多。6.2 AT模式为什么不需要写补偿代码Seata支持好几种事务模式其中最出名的是AT模式Automatic Transaction它最大的卖点是业务代码几乎零侵入一个注解就能开启全局事务不需要像TCC那样手写Try/Confirm/Cancel三个方法。AT模式的底层原理我拆开看其实一点都不神秘它本质上是2PC的思想 SAGA的补偿逻辑 自动生成SQL镜像。请求操作开启全局事务各个服务执行各自的本地业务SQL比如UPDATE stock SET count count - 100 WHERE product_id 1。执行这条SQL之前AT模式会先把这条数据当前的快照before image和更新后的快照after image保存到一张undo_log表里。这个保存动作和业务SQL在同一个本地事务里提交所以保证快照一定和业务操作绑定在一起。提交阶段如果全局事务所有分支都成功了TM通知TC提交TC会异步删除各分支的undo_log。注意此时本地业务SQL早就提交了不需要再锁库等指令。回滚阶段如果某个分支失败了需要全局回滚。TC通知各分支的RMRM根据undo_log里的before image生成一条反向的SQL把数据恢复成更新前的状态。这个过程不需要业务方写任何补偿逻辑完全是自动的。这里有个非常关键的设计开篇提到的全局锁。AT模式在回滚时用的是一种基于快照的补偿但如果有其他并发事务在这期间修改了同一行数据回滚就会出错。所以AT模式的Try阶段会获取一个全局锁这个锁保证同一时刻只有一个全局事务能操作某一行数据。所有分支事务提交后全局锁才会释放。这也是为什么AT模式的并发性能不如纯异步方案——全局锁天然的串行化约束和2PC的缩水版资源锁在原理上是相通的。6.3 理论模型如何指导实战排查一个真实的案例思路我前面为什么强调要把理论模型搞清楚因为在生产环境排查分布式事务问题时你会发现所有现象都能在理论模型里找到对应答案。有一次我处理一个Seata的回滚失败问题某个分支事务回滚时恢复的数据和预期不符。看起来莫名其妙的但如果你从AT模式的原理倒推问题其实很清晰——它本质上是补偿型事务的常见失效模式。排查思路是这样的回滚依赖undo_log里的before image。如果before image记录的是旧值但恢复时用反向SQL去update发现SQL影响了0行——说明这行数据已经被其他事务改过了这个时候应用层还没感知需要查一下是否有人绕过Seata直接操作了数据库或者有没有其他定时任务在同一段时间内改了同一张表。还有一种情况是AT模式回滚时找不到对应的undo_log记录。这不一定是bug很可能是全局事务提交后异步清理undo_log的任务已经把日志删掉了。所以排查时要注意时序问题不能一看报错就说框架有问题。要先确认事务处于哪种状态是全局提交了但清理任务没完成还是全局回滚但日志已经丢了。这就是理论模型对实战的指导价值它不会告诉你具体哪行代码出了问题但它能告诉你这类框架的出故障点一般会在哪几个位置。带着模型去看日志比漫无目的地搜关键字要高效得多。分布式事务的学习难就难在这里它不像调接口、写SQL那样有即时反馈而是一个先建立坐标系再逐个定位的过程。把CAP、BASE、2PC、TCC、SAGA、本地消息表、事务消息这条线理清楚再去看Seata、RocketMQ这些具体实现你会发现一切都串联起来了不再是一堆孤立的概念。
返回列表