前言:经常听到这个词,来学习一下,从网上找了找,发现一个叫无敌码农的微信公众号有两篇文章不错,参考了他的如下两篇文章,这篇博客主要是转载他的这两篇文章,对一些部分进行微调,感觉写的很好,其它的文章也不错,可以关注下(毕竟不能白嫖不是),后面的博客中本人计划用代码实现下面说的RocketMQ分布式事务。
分布式事务之深入理解什么是2PC、3PC及TCC协议
分布式事务之如何基于RocketMQ的事务消息特性实现分布式系统的最终一致性
目录
第一章 什么是分布式事务
注:我这里就是总结下,关于什么是分布式事务可以去看https://blog.csdn.net/bjweimengshu/article/details/79607522
1.1 什么是数据库事务
参考:https://www.runoob.com/mysql/mysql-transaction.html
比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务。
数据保持以下的特性
-
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
-
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
-
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
-
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
在单一的mysql数据库中,我们通过加锁、事务的开启关闭等等很容易实现事务的ACID特性。
1.2 分布式事务
在分布式的情况下,事务要保持ACID的特性就比较难了。
比如买东西的步骤为:检查库存-扣库存-生成订单,我们将生成订单进行异步,即检查库存和扣库存之后直接返回用户购买成功,生成订单交给另外的服务器来做,这样可能出现的问题就是库存生成成功了,但是订单生成失败了,这样就会导致事务的不一致。
第二章 分布式事务的解决方法
2.1 XA分布式事务协议
参考小灰灰的:https://blog.csdn.net/bjweimengshu/article/details/79607522
分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。
XA协议包含两阶段提交(2PC)和三阶段提交(3PC)两种实现。
2.1.1 二阶段提交
这个类似于剑网3的团本的团队确认功能,团长发起团队确认请求,当所有成员都确认就位后,团长发出开打指令,当有人点没就位时,团长就会说,等一下。
在XA协议中包含着两个角色:事务协调者和事务参与者。让我们来看一看他们之间的交互流程:
成功流程:
阶段1:
在XA分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。
在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。
当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。
阶段2:
在XA分布式事务的第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。
接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。
当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。
失败流程:
第一阶段:
还是协调者发送prepare,当有参与者执行失败后返回fail
第二阶段:
于是在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。
2.1.2 二阶段提交的问题
1.性能问题
XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
2.协调者单点故障问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
3.丢失消息导致的不一致问题。
在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
2.1.3 三阶段提交
XA三阶段提交在两阶段提交的基础上增加了CanCommit阶段,并且引入了超时机制。一旦事物参与者迟迟没有接到协调者的commit请求,会自动进行本地commit。这样有效解决了协调者单点故障的问题。但是性能问题和不一致的问题仍然没有根本解决。
三阶段是这样的:
第一阶段:CanCommit阶段:
这个阶段类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作,事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。
第二阶段:PreCommit阶段:
在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
否则,如果阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程中超时(2PC中只有协调者可以超时,参与者没有超时机制)。整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。
第三阶段:DoCommit阶段:
在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”-》“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。
相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务
总结:相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制(个人并没有找到2PC阶段协调者的超时机制,猜想应该是某个参与者超时,协调者让其它参与者放弃)。这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
2.2 TCC事务
TCC与2PC、3PC一样,只是分布式事务的一种实现方案而已,在TCC中我们需要自己实现Try接口、Confirm接口、Cancel接口,然后由分布式事务协调者进行调用。
TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)"。它分为三个操作:
-
Try阶段:主要是对业务系统做检测及资源预留。
-
Confirm阶段:确认执行业务操作。
-
Cancel阶段:取消执行业务操作。
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。
2.2 MQ事务
这章主要是使用RocketMq的事务消息机制来完成分布式事务
2.2.1 为什么要使用MQ
这里本人之前突然好奇过为什么要使用MQ,消息要发送到MQ里,消费者再从MQ中去拿,为什么不生产者直接通过http调用消费者接口呢,想了下,
1.直接调用http接口的话需要等待接口返回一些值后再进行操作,这算是耦合
2.其次在调用的过程中,出现了异常的话没有办法处理,比如生产者掉消费者接口后,消费者挂掉了,那生产者将不知道是消费者做完对应的操作没有,如果是生产者挂掉了,那么消费者是否要继续执行下面的操作呢,这也是个问题
所以用MQ还是很有必要的。
2.2.2 没有事务消息机制
这里假如没有事务消息机制,MQ的作用仅仅是把消息从生产者发送到消费者,这就需要我们实现一个可靠消息服务,这个可靠消息服务的作用不是MQ的作用,它主要是决定是否要把消息发送给MQ。
假设现在我们的场景是这样的,以某互联网公司的用户余额充值为例,因为有充返活动(充值100元赠送20元),优惠比较大,用户Joe禁不住诱惑用支付宝向自己的余额账户充值了100元,支付成功后Joe的余额账户有了120元钱。
以上的场景主要是有两个服务,一个是支付服务,把银行的钱充到支付宝,一个是余额服务,进行余额的修改,我们要保证这两个事务的一致性。
具体的流程如下:
1. 支付服务在进行本地数据库事务操作前,会先发送一个状态为“待确认”的消息至可靠消息服务,而不是直接将消息投递到MQ服务的指定Topic。可靠消息服务此时会将该消息记录到自身服务的消息数据库中(消息状态为->待确认)
2. 如果支付服务本地数据库事务执行成功,则继续向可靠消息服务发送消息确认消息,此时可靠消息服务就会正式将消息投递到MQ服务,并且同时更新消息数据库中的消息状态为“已发送”。(注意,这里可靠消息服务更新消息状态与投递消息至MQ也必须是在一个原子操作中,即消息投递成功则一定要将消息状态更新为“已发送”,所以在编程的细节中,可靠消息服务一般会先更新消息状态,然后再进行消息投递,这样即使消息投递失败,也可以对消息状态进行回滚->“待确认”,相反如果先进行消息投递再更新消息状态,可能就不好控制了)。
3. 如果支付服务数据库事务执行失败,则需要向可靠消息服务发送消息删除消息,可靠消息服务此时就会将消息删除,这样就意味着事务在支付消息投递过程中就被回滚了,而流程也就此结束了,此时支付服务可以需要通过业务逻辑的设计进行重发,这个就不再分布式事务的讨论范畴了。
此时消息已被成功投递到了MQ服务的指定Topic。
4. 在正常的流程中,余额服务等待消费Topic的消息,收到消息后进行自身本地数据库事务的处理,如果处理成功则会主动通知可靠消息服务,可靠消息服务此时就会将消息的状态更新为“已完成”。
上面就是不用事务消息的大体流程了,在这个流程中,大概会出现两个问题:
(1)但是在发送确认消息至可靠消息服务的过程中,以及可靠消息服务在投递消息至MQ服务的过程中,还是会存在失败的风险,这样的话还是会导致支付服务更新了状态,但是用户余额服务连消息都没有收到的情况发生。
在这里可靠消息服务需要启动相应的后台线程,不断轮训消息的状态,这里会轮训消息状态为“待确认”的消息,并判断该消息的状态的持续时间是否超过了规定的时间,如果超过规定时间的消息还处于“待确认”的状态,就会触发上游服务状态询问机制。
可靠消息服务就会调用上游服务提供的相关借口,询问这笔消息的处理情况,如果这笔消息在上游服务处理成功,则后台线程就会继续触发上图中的步骤2,更新消息状态为“已发送”并投递消息至MQ服务;反之如果这笔消息上游服务处理失败,可靠消息服务则会进行消息删除。通过这样以上机制就确保了“上游服务本地事务成功处理+消息成功投递”处于一个原子操作了。
(2)如果余额服务处理失败余额服务就无法再主动向可靠消息服务发送通知消息了。
与消息投递过程中的异常逻辑一样,可靠消息服务也会启动相应的后台线程,轮询一直处于“已发送”状态的消息,判断状态持续时间是否超过了规定时间,如果超时,可靠消息服务就会再次向MQ服务投递此消息,从而确保消息能被再次消费处理。(注意,也可能出现下游服务处理成功,但是通知消息发送失败的情况,所以为了确保幂等,下游服务也需要在业务逻辑上做好相应的防重处理)。
2.2.3 使用rocketMQ提供的事务消息机制
通过上面我们看到可靠消息服务是比较重要的,如果我们自己实现一个可靠消息服务会比较麻烦,RocketMQ提供了可靠消息服务。
我们主要是要实现这样的效果:发送消息的一方我们称之为Producer,接收消费消息的一方我们称之为Consumer。如果Producer自身业务逻辑本地事务执行成功与否希望和消息的发送保持一个原子性(也就是说如果Producer本地事务执行成功,那么这笔消息就一定要被成功的发送到RocketMQ服务的指定Topic,并且Consumer一定要被消费成功;反之,如果Producer本地事务执行失败,那么这笔消息就应该被RocketMQ服务器丢弃)
1、Producer选择使用RockerMQ提供的事务消息方法向RocketMQ服务发送事务消息(设置消息属性TRAN_MSG=TRUE);
2、RocketMQ服务端在收到消息后会判断消息的属性是否为事务消息,如果是普通消息就直接Push给Consumer;如果是事务消息就会对该消息进行特殊处理设置事务ID,并暂时设置该消息对Consumer不可见,之后向Producer返回Pre消息发送状态(SEND_OK)。
3、之后Producer就会开始执行本地事务逻辑,并设置本地事务处理状态后向RocketMQ服务器发送该事务消息的确认/回滚消息(COMMIT_MESSAGE/ROLLBACK_MESSAGE)。
4、RocketMQ服务器根据该笔事务消息的本地事务执行状态决定是否将消息Push给Consumer还是删除该消息。
5、之后Consumer就会消费该消息,执行Consumer的本地事务逻辑,如果执行成功则向RocketMQ返回“CONSUME_SUCCESS”;反之出现异常则需要返回“RECONSUME_LATER”,以便RocketMQ再次Push该消息,这一点在实际编程中需要控制好。
正常情况下以上就是RocketMQ事务消息的基本运行流程了,但是从异常情况考虑,理论上也是存在Producer迟迟不发送确认或回滚消息的情况。与可靠消息服务一样,RocketMQ服务端也会设置后台线程去扫描消息状态,之后会调用Producer的本地checkLocalTransaction函数获取本地事务状态后继续进行第3步操作。