六种分布式事务解决方案
Posted cj_eryue
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了六种分布式事务解决方案相关的知识,希望对你有一定的参考价值。
目录
事务的概念
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。
- 原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么都不执行。
- 一致性(Consistency),可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据,比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态。
- 隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。
- 持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。
而通俗意义上事务就是为了使得一些更新操作要么都成功,要么都失败。
分布式理论
CAP定理
在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」
一致性(C):
在分布式系统中的所有数据备份,「在同一时刻是否拥有同样的值」。(等同于所有节点访问同一份最新的数据副本)
可用性(A):
在集群中一部分节点「故障」后,集群整体「是否还能响应」客户端的读写请求。(对数据更新具备高可用性)
分区容错性(P):
即使出现「单个组件无法可用,操作依然可以完成」。
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用「至多只能同时支持上面的两个属性」。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
2PC
2PC(Two-phase commit protocol),二阶段提交(准备、提交)。2PC引入一个事务协调者的角色来协调管理各参与者的提交和回滚,是一种强一致性设计。
第一阶段,准备阶段(投票阶段):协调者会给各参与者发送准备(准备提交事务)的命令
同步等待所有资源的响应成功之后就进入提交阶段(提交阶段不一定是提交事务,也有可能是回滚事务)。
第二阶段:提交/回滚
如果第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事物都提交成功之后,返回事务执行成功。
如果在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务失败。
如果第二阶段提交失败了呢?
有两种情况:
1.第二阶段执行的是回滚事务操作:第二阶段如果失败则不断重试,直到所有参与者都回滚,不然那些在第一阶段准备成功的参与者会一直阻塞。
2.第二阶段执行的是提交事务操作:也是不断重试,直到所有参与者都提交成功。
协调者故障分析
发送准备命令之前挂了 | 事务还没开始,可接受 |
发送准备命令之后挂了 | 参与者阻塞 |
发送回滚事务命令之前挂了 | 参与者阻塞 |
发送回滚事务命令之后挂了 | 可能回滚成功,可能阻塞 |
发送提交事务命令之前挂了 | 参与者阻塞 |
发送提交事务命令之后挂了 | 可能提交成功,可能阻塞 |
协调者故障,通过选举得到新协调者
因为协调者单点问题,因此我们可以通过选举等操作选出一个新协调者来顶替。
如果处于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。
如果处于第二阶段,假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。
但是假如某个参与者挂了,协调者发送的是回滚命令,此时这个参与者执行了回滚,然后和协调者一起挂了,其他参与者又没收到请求,然后新协调者来了,但是新的协调者不知道的挂了的那个参与者
假设有个别参与者挂了!这就有点僵硬了,比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但它不知道挂了的那个参与者到底O不OK,所以它傻了。
问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。
虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不该发了?
但是就算协调者知道自己该发提交请求,那么在参与者也一起挂了的情况下没用,因为你不知道参与者在挂之前有没有提交事务。
如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。
如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。
所以说极端情况下还是无法避免数据不一致问题。
2PC出现单点问题的三种情况
(1)协调者正常,参与者宕机
由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。
解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。----3PC
(2)协调者宕机,参与者正常
无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.
解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
(3)协调者和参与者都宕机
- 发生在第一阶段: 因为第一阶段,所有参与者都没有真正执行commit,所以只需重新在剩余的参与者中重新选出一个协调者,新的协调者再重新执行第一阶段和第二阶段就可以了。
- 发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。新的协调者重新执行第一阶段和第二阶段操作。
- 发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者 发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! 2PC 无法解决这个问题。
2PC总结:
两阶段提交协议存在的问题:1、同步阻塞问题(所有的参与者资源和协调者资源都是被锁住的)。 2、单点故障。 3、数据不一致。
2PC 适用于数据库层面的分布式事务场景
3PC
3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
准备阶段(CanCommit):确认参与者状况
- 事务询问:协调者 向 参与者 发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待 参与者 的响应。
- 响应反馈:参与者 接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
预提交阶段(PreCommit):在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。这里的PreCommit阶段 跟上面的第一阶段是差不多的,只不过这里 协调者和参与者都引入了超时机制 (2PC中只有协调者可以超时,参与者没有超时机制)。
提交阶段(DoCommit):同2PC
我们知道 2PC 是同步阻塞的,上面我们已经分析了协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着。
那么引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。
然而超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。
从维基百科上看,3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。
新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。
所以说 3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。
但是这也只能让协调者知道该如果做,但不能保证这样做一定对,这其实和上面 2PC 分析一致,因为挂了的参与者到底有没有执行事务无法断定。
所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。
让我们总结一下, 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。
所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。
我再说下 3PC 我没有找到具体的实现,所以我认为 3PC 只是纯的理论上的东西,而且可以看到相比于 2PC 它是做了一些努力但是效果甚微,所以只做了解即可。
总结
相比较2PC而言,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?
这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,
自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。
XA协议
是一个基于数据库的分布式事务协议,其分为两部分:事务管理器
和本地资源管理器
。事务管理器
作为一个全局的调度者,负责对各个本地资源管理器统一号令提交
或者回滚
。二阶提交协议(2PC
)和三阶提交协议(3PC
)就是根据此协议衍生出来而来。如今Oracle
、mysql
等数据库均已实现了XA接口
。
补偿事务(TCC)
TCC
(Try-Confirm-Cancel)又被称补偿事务
,TCC
与2PC
的思想很相似,事务处理流程也很相似,但2PC
是应用于在DB层面,TCC则可以理解为在应用层面的2PC
,是需要我们编写业务逻辑来实现。
TCC
它的核心思想是:"针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)"。
Try阶段:预留,即资源的预留和锁定;
Confirm阶段:确认操作,真正的执行
Cancel阶段:撤销操作,可以理解为把预留阶段的动作撤销了
TCC的缺点:
- 应用侵入性强:TCC由于基于在业务层面,至使每个操作都需要有
try
、confirm
、cancel
三个接口。 - 开发难度大:代码开发量很大,要保证数据一致性
confirm
和cancel
接口还必须实现幂等性。
比如下一个订单减一个库存:
执行流程:
-
Try阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,
-
-
如果Try阶段「执行成功」,执行Confirm阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量
-
如果Try阶段「执行失败」,执行Cancel阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量
-
TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点:
-
1.「解决了协调者单点」,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
-
2.「同步阻塞」:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
-
3.「数据一致性」,有了补偿机制之后,由业务活动管理器控制一致性
总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的「增加」了业务代码的「复杂度」,因此,这种模式并不能很好地被复用。
本地消息表
本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
执行流程:
-
消息生产方,需要额外建一个消息表,并「记录消息发送状态」。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。
-
-
如果消息发送失败,会进行重试发送。
-
-
消息消费方,需要「处理」这个「消息」,并完成自己的业务逻辑。
-
-
如果是「业务上面的失败」,可以给生产方「发送一个业务补偿消息」,通知生产方进行回滚等操作。
-
此时如果本地事务处理成功,表明已经处理成功了
-
如果处理失败,那么就会重试执行。
-
-
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
消息事务
RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
可以看到消息事务实现的也是最终一致性。
对比本地消息表实现方案,不需要再建消息表,「不再依赖本地数据库事务」了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的「只有阿里的 RocketMQ」。
最大努力通知
其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。
总结
可以看出 2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。
而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。
本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。
以上是关于六种分布式事务解决方案的主要内容,如果未能解决你的问题,请参考以下文章