在分布式系统中,多个服务配合完成一个流程,不同服务执行结果不一定都成功,这时候就会产生问题。比如订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存, 如果订单服务执行成功,但是库存服务执行失败没有扣减库存,那么就会出现超卖现象。
针对此问题,引入分布式事务进行控制。在深入了解分布式事务前,需要先了解一些理论。
CAP 定理又被称作布鲁尔定理,是加州大学伯克利分校的计算机科学家埃里克·布鲁尔(Eric Brewer)在 2000 年的 ACM PODC 上提出的一个猜想。2002 年,麻省理工学院的赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。简单来说:在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
假设2个节点 N1 和 N2,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 需要返回 Error,提示客户端 C“系统现在发生了错误”,这种处理方式违背了可用性(Availability)的要求,因此 CAP 三者只能满足 CP。
还是 N1、N2 两个节点,为了保证可用性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 将当前自己拥有的数据 x 返回给客户端 C 了,而实际上当前最新的数据已经是 y 了,这就不满足一致性(Consistency)的要求了,因此 CAP 三者只能满足 AP。注意:这里 N2 节点返回 x,虽然不是一个“正确”的结果,但是一个“合理”的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据而已。
BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。满足 BASE 理论的事务,我们称之为“柔性事务”。
有了这些理论基础,就可以往下探讨分布式事务解决方案了。
在了解XA方案前,首先了解什么是XA。XA 是由 X/Open组织(即现在的 Open Group )定义的分布式事务处理模型,它包括:
XA 规范定义了交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。而XA接口函数由数据库厂商提供。 具体使用步骤如下:
基于XA规范实现的分布式解决方案主要有有 2PC 和 3PC。
二阶段提交协议的算法思路可以概括为:每个参与者(RM)将操作成败通知协调者(TM),再由协调者根据所有参与者的反馈情报,决定各参与者是否要提交操作还是中止操作。它的具体工作内容是:
2PC 提高了数据一致性的概率,实现成本较低。但是缺点也比较明显:
阿里自研的 Seata 中间件就是基于 2PC 实现的。在 Seata 中,分布式事务的执行流程:
三阶段提交协议是二阶段提交协议的改进版本,它相较于 2PC 来说,增加了 CanCommit阶段 并引入了超时机制。具体工作流程是:
通过加入 CanCommit阶段进行尝试,避免了像 2PC那样在第一阶段就尝试执行事务并生成undo log。此外在 3PC 中的 DoCommit 阶段,如果事务参与者迟迟没有收到协调者的 commit 通知,就会自动进行本地commit,相对有效地解决了协调者单点故障的问题。(因为参与者能进入第三阶段,说明参与者在第二阶段已经收到了协调者的PreCommit消息,而协调者发送PreCommit消息的前提是,在第二阶段开始之前,协调者收到了所有参与者的CanCommit响应都是Yes。这说明所有的参与者都认为自己有能力正确处理处理事务。所以,当参与者进入第三阶段后,由于网络超时等原因,参与者会在等待超时后自动提交,因为参与者相信,直接提交的话,有很大几率是整个分布式事务是成功的。)
TCC方案是采用最终一致性的方式实现的服务层柔性分布式事务方案,采用Try、Confirm、Cancel三个阶段来处理分布式事务,每个阶段具体负责的工作为:
如果在 Confirm 和 Cancel 阶段失败,TCC 会不停重试调用Confirm 或者 Cancel 方法,直到成功为止。
使用 TCC 方案时,有三个典型问题:空回滚、幂等和悬挂。
当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行 Try 阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的 Cancel 方法,从而形成空回滚。
解决思路很简单,就是需要知道一阶段是否执行,如果执行了,那就是正常回滚,否则就执行空回滚。前面已经说过 TM 在发起全局事务时生成全局事务记录,全局事务ID 是贯穿整个分布式事务调用链条的,只需要再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。在Cancel 里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。
为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
解决思路在分支事务记录中增加执行状态,每次执行前都查询该状态。
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。出现原因是在 RPC 调用分支事务 Try 时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,业务资源预留后没法继续处理。
解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
这种方案需要考虑以下问题:
此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。这种方案能够保障消息可靠、最终一致。
顾名思义,发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
以上就是分布式事务的主流解决方案。分布式事务问题无可避免,只能根据具体业务场景去选择设计合理的方式。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。