【分布式面试高频】- 04 分布式事务的解决方案(2PC TCC 可靠性消息 最大努力通知)
【分布式面试高频】- 04 分布式事务的解决方案(2PC TCC 可靠性消息 最大努力通知)4.1网络二将军问题这里给出一个二将军问题的简化版本:一支白军被围困在一个山谷中,山谷的左右两侧是蓝军。困在山谷中的白军人数多于山谷两侧的任意一支蓝军,而少于两支蓝军的之和。若一支蓝军对白军单独发起进攻,则必败无疑;但若两支蓝军同时发起进攻,则可取胜。两只蓝军的总指挥位于山谷左侧,他希望两支蓝军同时发起进攻
【分布式面试高频】- 04 分布式事务的解决方案(2PC TCC 可靠性消息 最大努力通知)
4.1 网络二将军问题
这里给出一个二将军问题的简化版本:
一支白军被围困在一个山谷中,山谷的左右两侧是蓝军。困在山谷中的白军人数多于山谷两侧的任意一支蓝军,而少于两支蓝军的之和。若一支蓝军对白军单独发起进攻,则必败无疑;但若两支蓝军同时发起进攻,则可取胜。两只蓝军的总指挥位于山谷左侧,他希望两支蓝军同时发起进攻,这样就要把命令传到山谷右侧的蓝军,以告知发起进攻的具体时间。假设他们只能派遣士兵穿越白军所在的山谷(唯一的通信信道)来传递消息,那么在穿越山谷时,士兵有可能被俘虏。
只有当送信士兵成功往返后,总指挥才能确认这场战争的胜利(上方图)。现在问题来了,派遣出去送信的士兵没有回来,则左侧蓝军中的总指挥能不能决定按命令中约定的时间发起进攻?
答案是不确定,派遣出去送信的士兵没有回来,他可能遇到两种状况:
- 命令还没送达就被俘虏了(中间图),这时候右侧蓝军根本不知道要何时进攻;
- 命令送达,但返回途中被俘虏了(下方图),这时候右侧蓝军知道要何时进攻,但左侧蓝军不知道右侧蓝军是否知晓进攻时间。
类似的问题在计算机网络中普遍存在,例如发送者给接受者发送一个 HTTP 请求,或者 MySQL 客户端向 MySQL 服务器发送一条插入语句,然后超时了没有得到响应。请问服务器是写入成功了还是失败了?答案是不确定,有以下几种情况:
- 可能请求由于网络故障根本没有送到服务器,因此写入失败;
- 可能服务器收到了,也写入成功了,但是向客户端发送响应前服务器宕机了;
- 可能服务器收到了,也写入成功了,也向客户端发送了响应,但是由于网络故障未送到客户端。
无论哪种场景,在客户端看来都是一样的结果:它发出的请求没有得到响应。为了确保服务端成功写入数据,客户端只能重发请求,直至接收到服务端的响应。
网络二将军问题的存在使得消息的发送者往往要重复发送消息,直到收到接收者的确认才认为发送成功,但这往往又会导致消息的重复发送。 例如电商系统中订单模块调用支付模块扣款的时候,如果网络故障导致二将军问题出现,扣款请求重复发送,产生的重复扣款结果显然是不能被接受的。因此要保证一次事务中的扣款请求无论被发送多少次,接收方有且只执行一次扣款动作,这种保证机制叫做接收方的幂等性。
4.2 2PC两阶段提交
两阶段提交(2PC), 通过 引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
(1)准备阶段:协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。
(2)提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知并让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是:在准备阶段,参与者只是执行了事务,但是还未提交。只有在提交阶段受到协调者发来的通知后,才进行提交或者回滚。
存在问题:
- 性能差。在准备阶段,要等待所有的参与者返回,才能进入阶段二。在这期间,各个参与者上面的相关资源被排它锁锁住,参与者试图使用这些资源的本地事务只能等待。因为存在这种同步阻塞问题,所以影响了各个参与者的本地事务并发度。
- 在准备阶段完成后,如果协调者宕机,所有的参与者都收不到提交或者回滚命令,导致所有参与者“不知所措”。
- 在提交阶段,协调者向所有的参与者发送了提交指令,但是如果如果一个参与者未返回ACK,那么协调者不知道这个参与者内部发生了什么(由于网络二将军问题的存在,这个参与者可能根本没收到提交指令,一直处于等待接收提交指令的状态;也可能收到了,并成功执行了本地提交,但返回的 ACK 由于网络故障未送到协调者上),也就无法决定下一步是否进行全体参与者的回滚。
4.4 3PC三阶段提交
三阶段提交,相对比2PC来说增加了CanCommit阶段和超时机制。如果一段时间内,没有收到协调者的commit,那么就会自动进行commit,解决了2PC单点故障问题。
流程是:
- 第一阶段:【CanCommit阶段】这个阶段所做的事情很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。
- 如果都返回yes,则进入第二阶段
- 有一个返回no或者等待响应超时,则中断事务,并向所有参与者发送abort请求
- 第二阶段:【PreCommit阶段】此时协调者会向所有参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“ACK”表示我已经准备好提交了,并等待协调者的下一步指令。
- 第三阶段:**「DoCommit阶段」**在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。
三阶段提交有两个改进点:
- 引入超时机制。同时在协调者和参与者都引入了超时机制;
- 3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有cancommit,precommit和docommit三个阶段。
3PC利用超时机制解决了2PC的同步阻塞问题,避免资源被永久锁定,进一步增强了整个事务过程的可靠性。但是3PC同样无法应用类似的宕机问题,只不过出现多数据源中数据不一致问题的概率更小。
4.4 TCC方案
TCC是一种解决多个微服务之间的分布式事务问题的方案。TCC是Try,Confirm,Cancel三个词的缩写,其本质上是一个应用层面上的2PC,同样也分为两个阶段:
- 准备阶段:协调者调用所有的微服务提供的try接口,将整个全局事务涉及到的资源锁定住,若锁住成功try接口向协调者返回yes;
- 提交阶段:若所有服务的try接口在阶段一都返回yes,则进入提交阶段,协调者调用所有服务的confirm接口,各个服务进行事务提交。如果有任何一个服务的try接口在阶段一返回no或者超时,则协调者调用所有服务的cancel接口;
这里有个关键问题,既然TCC是一种服务层面上的2PC,那么它是如何解决2PC无法应对的宕机问题的缺陷呢?
答案是不断尝试。
由于try操作锁住了全局事务涉及的所有资源,保证了业务操作的所有前置条件得到了满足,因此无论是confirm阶段失败还是cancel阶段失败都能通过不断重试直到confirm或cancel成功。(所谓成功就是所有的服务都对confirm或者cancel返回了ACK)。
这里还有个关键问题,在不断重试confirm和cancel的过程中(考虑到网络二将军问题的存在)有可能重复进行了confirm或cancel。因此还要再保证confirm和cancel操作具有幂等性,也就是在整个全局事务中,每个参与者只进行一次confirm或者cancel。
实现confirm和cancel操作的幂等性,有很多解决方案。例如每个参与者可以维护一个去重表,记录每个全局事务是否进行过confirm或cancel,若已经进行过,则不再重复执行。
4.5 2PC与TCC的对比
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此 外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
4.6 可靠消息最终一致性
(1)什么是可靠消息最终一致性
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能 够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致
此方案是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件 之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事 务问题。
因此可靠消息最终一致性方案要解决以下几个问题:
a.本地事务与消息发送的原子性问题
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实 现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最 终一致性方案的关键问题。
b.事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
c.消息重复消费的问题
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重 复消费。
要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
(2)解决方案:本地消息表方案
此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后 通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明:
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
交互流程如下:
1、用户注册
用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致) 下边是伪代码
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性
2、定时任务扫描日志
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息 中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
3.消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
4.7 最大努力通知
最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:
1、账户系统调用充值系统接口
2、充值系统完成支付处理向账户系统发起充值结果通知 若通知失败,则充值系统按策略进行重复通知
3、账户系统接收到充值结果通知修改充值状态。
4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
4.8 最大努力通知与可靠消息一致性有什么不同
(1)解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发送到接收通知放,消息的可靠性由通知方来保证。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方
(2)两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
(3)技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
4.9 分布式事务对比分析
在了解各种分布式事务的解决方案后,我们了解到各种方案的优缺点:
2PC 最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其 阻塞机制和最差时间复杂度高, 因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并 发较高以及子事务生命周期较长 (long-running transactions) 的分布式服务中。
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实 现不同的回滚策略。典型的使用场景:满,登录送优惠券等。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消 息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注 册送积分,登录送优惠券等。
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。
总结: 在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务ACID做对比。
无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。
更多推荐
所有评论(0)