阅读 496

springCloud的了解(四)—分布式事务问题

                                                更新时间:2019年7月20日
复制代码

前言

springCloud系列文章:

springCloud的了解(一):juejin.im/post/5d2eca…

springCloud的了解(二):juejin.im/post/5d2fd0…

springCloud的了解(三):juejin.im/post/5d3123…

昨天,我们又学习了springCloud的剩余的大部分组件,config配置中心,Feign声明式服务调用,Turbin集群监控,Bus消息总线。

关于SpringCloud的组件内容,我们暂时先告一段落,当然在后续的文章中,我会陆续的深入到组件的源代码分析和组件的具体作用中去。今天我们要开始学习在springCloud中的分布式事务问题。

著名架构师Chris Richardson所言,目前springCloud落地主要存在的困难有如下几方面:

1.单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂。

2.系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。

3.微服务数量众多,其测试、部署、监控等都变的更加困难。

随着RPC框架的成熟,第一个问题已经逐渐得到解决。例如dubbo可以支持多种通讯协议,springcloud可以非常好的支持restful调用。对于第三个问题,随着docker、devops技术的发展以及各公有云paas平台自动化运维工具的推出,微服务的测试、部署与运维会变得越来越容易。

那么,详细点讲,什么是分布式事务问题呢?

在springCloud中,有时候我们写一个看似简单的功能,在程序的内部却有可能调用到多个其他的微服务,而这些微服务不但有可能是集群性部署,而且数据库也有可能进行分库分区,且数据库也会进行集群部署,那么也会调用多个数据库。

那么,当我们在进行这么一个简单的功能的时候,一旦某一步骤的调用出现了问题,数据没有传达到位,那么极有可能使整个程序的数据不统一,不一致了。

简单点说,在分布式系统中,一个大的操作由无数个小的操作组成,那么分布式事务就是要保证这些小的操作,要么全部都能成功,要么全部都是失败,从而保证了数据的一致性。

本质上来说,分布式事务就是为了保证不同数据库或消息系统的数据一致性。

这就是任何一个开发者都必须要面对的一个问题——分布式事务问题!

*ps:在正式开始之前,我又要给大家推荐一首歌了,盘尼西林的歌,《雨夜曼彻斯特》,也是从综艺节目《乐队的夏天》中听到的,希望大家会喜欢。(^_−)☆

分布式事务分析

在开始具体的分析之前,我们要先对分布式事物的发展历程有一个具体的了解。

在早期的时候,分布式事务的解决方案是由2pc(两阶段提交)以及相应的变种3PC来实现(因为2PC有致命的问题,3PC通过拆分2PC的第一阶段避免了极端情况下的问题)。

随着互联网的发展,这种方案已经不能我们对性能的追求,尤其是阿里巴巴的蚂蚁金融,对事务的要求是极其严格的,于是在2PC的基础上出现了经典的TCC模式,来保证事务的一致性。

但是TCC模式对编程的要求极高,于是后面又出现了通过消息中间件来保证事务最终一致性的方案来解决事务问题。

直到2017年,阿里巴巴出现了GTS方案来保证事务强一致性的方案,而且提供了许多的服务给互联网使用者,应该是当前最先进的分布式事务处理方案。

这是整个分布式事务的发展历程。

补充一下内容:

强一致性:
当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的
更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就
保证能读到什么。根据 CAP理论,这种实现需要牺牲可用性。

弱一致性:
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。用户读
到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不
一致性窗口”。系统在数据写入成功之后,不承诺立即可以读到最新写入
的值,也不会具体的承诺多久之后可以读到。

最终一致性:
是弱一致性的一种特例。系统保证在没有后续更新的前提下,系统最终返
回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主
要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终
一致性系统。
复制代码

一. 2PC两阶段提交方案

在2pc中,我们假设定有两个角色,一个是协调者,一个是参与者。

参与者负责具体执行操作,一般来说就会打开本地数据库事务,然后开始执行数据库本地事务,但在执行完成后并不会立马提交数据库本地事务,而是将每次执行的情况(成功或者失败)报告给协调者,协调者通过总结参与者报告的内容进行分析,最后决定事务是否继续全部执行,或者是全部回滚。

具体来讲,可以将整个流程分为两个阶段。

第一阶段是请求阶段,是参与者将自己执行的情况发送给协调者。参与者发送的,有可能是成功,有可能是失败(程序执行出现故障,如果没有收到参与者的消息,那么就是超时,默认为失败)。

第二阶段是提交阶段,协调者获取到了所有参与者发送的执行结果,通过分析这些执行结果,协调者来判断,是整个程序继续执行下去,还是进行全体回滚,然后将这个决定发送给所有参与者。

这个方案的缺点是非常明显的:

1.要等到所有的参与者执行完操作,而且参与者发送自己的结果给协调者
的时候,这两点占据了太多的公共资源。(因为要进行通信,协调者和参
与者是份属不同的微服务)
2.协调者在整个体系中太重要了,一旦出错,会导致所有的参与者进入到
阻塞状态。(实际上协调者即便宕机,但是在2PC中,会自动从参与者选
举出一个协调者,但是参与者阻塞的状态无法得到解决。)
3.当协调者统计参与者返回的数据进行分析,得出结果后,将结果发送给
所有的参与者,但是假如一部分的参与者接受结果的时候出错或者通信失
效,没有得到协调者发送的最终结果,那么会导致数据的最终不一致。
复制代码

也因为这些缺点,后面就出现了2PC的变种——3pc。

二. 3PC三阶段提交方案

三阶段提交方案,在二阶段提交方案的基础上,让参与者和协调者都有了超时机制,而且将2PC的第一个阶段拆分为了两个阶段,先询问,再锁定资源,然后再真正提交。

第一阶段:协调者向参与者发送询问,参与者返回自己的结果给协调者,成功或者失败。

第二阶段:协调者根据参与者返回的结果,进行分析。将会出现两种结果:

1.假如参与者返回的都是成功Yse,那么协调者将会发送预执行消息给参
与者,并且协调者进入到预执行准备阶段。而参与者接收到协调者发送
的指令后,将会把这些操作指令存储到数据库的事务日志中,并且进行
预执行操作。如果参与者成功完成了指令,那么就会返回结果ACK给协
调者,并且等待最终结果。
2.如果有一个参与者返回的是失败No,或者有一个参与者超时了没有返
回结果,那么协调者将会发送中断事务的指令给所有的参与者,让参与
者进行事务中断。
复制代码

第三阶段:当协调者受到了所有参与者的ACK结果,那么协调者就会从预执行准备阶段变成正式执行阶段,并且把正式执行的指令发送给所有的参与者。参与者收到指令,正式执行事务操作,且成功后,再一次返回ACK成功指令给协调者。同样,如果有一个参与者返回的是失败No,或者有一个参与者超时了没有返回结果,那么协调者将会发送中断事务的指令给所有的参与者,让参与者进行事务中断。

在2PC的准备阶段和提交阶段之间,插入预提交阶段,这是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

这个方案的缺点:

当协调者发送中断事务的指令给参与者的时候,只有一个参与收到了执
行事务中断的指令,其他参与者没有收到,那么其他参与者会默认正式
执行事务操作。这样就会导致了整个系统状态和数据的不一致了。(一
旦事务参与者迟迟没有收到协调者的正式执行请求,就会自动进行本地
正式执行,这样相对有效地解决了协调者单点故障的问题,这就是3PC
加入的参与者的超时)
复制代码

三. TCC方案—补偿事务

前面讲的解决分布式事务的方案,2PC和3PC都是在DB(数据库)层面解决问题,是基于XA协议。 科普一下:

XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:
事务管理器和本地资源管理器。其中本地资源管理器往往由数据
库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而
事务管理器作为全局的调度者,负责各个本地资源的提交和回滚
。总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协
议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点
,那就是性能不理想,特别是在交易下单链路,往往并发量很高
,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想
,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录
prepare阶段日志,主备切换回导致主库与备库数据不一致。许
多Nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
复制代码

作为本身是基于XA协议的2PC和3PC,自然也继承了XA协议的所有问题,于是后面出现了基于业务层的TCC事务处理方案。

TCC方案,确实与2PC协议有极大的相同处,甚至对比这两张方案的流程,也基本上是一致的,它们最主要的区别是2PC是基于DB层面的处理方案,而TCC是基于业务层面的处理方案。

TCC的三个阶段如下:

Try阶段:主要是对业务系统做检测及资源预留。

Confirm阶段:确认执行业务操作。

Cancel阶段:取消执行业务操作。

由于TCC模式,是成长在业务层面的方案,那么它确实是需要和业务代码进行耦合的。

1.所有事务参与方都需要实现try,confirm,cancle接口。
2.事务参与者向事务协调者发起事务请求,事务协调者调用
  所有事务参与者的try方法完成资源的预留,这时候并没有
  真正执行业务,而是为后面具体要执行的业务预留资源,这
  里完成了一阶段。
3.如果事务协调者发现有参与者的try方法预留资源时候发现
  资源不够,则调用参与者的cancle方法回滚预留的资源,需要
  注意cancle方法需要实现业务幂等,因为有可能调用失败(比
  如网络原因参与者接受到了请求,但是由于网络原因事务协调
  器没有接受到回执),那么就要进行重试。
4.如果事务协调者发现所有参与者的try方法返回都OK,则事务
  协调者调用所有参与者的confirm方法,不做资源检查,直接
  进行具体的业务操作。
5.如果协调者发现所有参与者的confirm方法都OK了,则分布式
  事务结束。
6.如果协调者发现有些参与者的confirm方法失败了,或者由于
  网络原因没有收到回执,则协调器会进行重试。
复制代码

在第六步的时候,这里如果重试一定次数后还是失败,那么就会出现不一致,一般称为 heuristic exception。

heuristic exception 是不可杜绝的,但是可以通过设置合适的超时时间,以及重试频率和监控措施使得出现这个异常的可能性降低到很小。我们最常见的是做事务补偿。因为我们操作的每一步都是要事务日志记录在数据库,我们可以在最后调出事务日志,通过事务日志进行补偿。(说白了,就是进行人工服务)

优点在这:

解决了跨应用业务操作的原子性问题,在诸如组合支付、账务
拆分场景非常实用。TCC实际上把数据库层的二阶段提交上提
到了应用层来实现,对于数据库来说是一阶段提交,规避了
数据库层的2PC性能低下问题。
复制代码

缺点在这:

TCC的Try、Confirm和Cancel操作功能需业务提供,开发成本
高,对程序的侵入性高。此外,其实现难度也比较大,需要
按照网络状态、系统故障等不同的失败原因实现不同的回滚
策略。为了满足一致性的要求,confirm和cancel接口还必
须实现幂等。
复制代码

由TCC模式,引申出了一种事务处理方案,SAGA模式。这种模式最主要的不同是使用一个反向的业务操作,来撤销之前的业务操作。SAGA模式,try阶段直接操作目标字段,不要进行预处理,和TCC模式相比,SAGA不需要confirm操作。大家如果感兴趣的,可以去了解一下。

四. 消息中间件RocketMQ—最终一致性方案

基于消息中间件的事务处理方案和TCC一样,都是柔性的事务处理方案,只需要保证事务的最终一致性。

先给大家一张图片,普通消息的流程图,大家可以通过图片中的编号顺序来了解整个流程: (图片来自我参考的博客:www.jianshu.com/p/04bad986a…

..消息生成者发送消息。
..MQ收到消息,将消息进行持久化,在存储中新增一条记录
..返回ACK给消费者。
..MQ提交消息给对应的消费者,然后等待消费者返回ACK。
..如果消息消费者在指定时间内成功返回ACK,那么MQ认为。
  消息消费成功,在存储中删除消息,即执行第6步;
..如果MQ在指定时间内没有收到ACK,则认为消息消费失败
  ,会尝试重新push消息,重复执行4、5、6步骤。
..MQ删除消息。
复制代码

我们需要的是数据的一致性,那么普通的消息的流程中,哪些流程会导致数据的不一致呢?

1.第1步,当消息生产者处理业务成功,但是因为宕机的原因,
  消息没有发送到MQ那里,导致了事务消息不一致,从而导致
  了生产者和消费者的数据不一致了。
2.第4步,MQ发送给消息消费者的时候,因为通信原因,到这
  消息消费者没有收到消息,也导致了消息生产者和消息消
  费者的数据不一致。
3.消息消费者处理业务成功,发送了ACK消息给MQ,但是MQ
  因为处理超时了,返回的是失败消息给消息生产者,导致
  生产者事务回滚,也导致了消息生产者和消息消费者的数
  据不一致。
复制代码

远程调用,结果最终可能为成功、失败、超时;而对于超时的情况,处理方最终的结果可能是成功,也可能是失败,调用方是无法知晓的。

那么,通过事务消息来处理呢?

现在目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、
RocketMQ等,只有RocketMQ支持事务消息。原因就是RocketMQ
是阿里巴巴的消息中间件。
复制代码

由于传统的处理方式无法解决消息生成者本地事务处理成功与消息发送成功两者的一致性问题,因此事务消息就诞生了,它实现了消息生成者本地事务与消息发送的原子性,保证了消息生成者本地事务处理成功与消息发送成功的最终一致性问题。

(图片来自我参考的博客:www.jianshu.com/p/04bad986a…

.. 事务消息与普通消息的区别就在于消息生产环节,生产者
   首先预发送一条消息到MQ(这也被称为发送half消息)。
.. MQ接受到消息后,先进行持久化,则存储中会新增一条状
   态为待发送的消息。
.. 然后返回ACK给消息生产者,此时MQ不会触发消息推送事件。
.. 生产者预发送消息成功后,执行本地事务。
.. 执行本地事务,执行完成后,发送执行结果给MQ。
.. MQ会根据结果删除或者更新消息状态为可发送。
.. 如果消息状态更新为可发送,则MQ会push消息给消费者,
   后面消息的消费和普通消息是一样的。
复制代码

注意点:

由于MQ通常都会保证消息能够投递成功,因此,如果业务没有及时返回ACK结果,那么就有可能造成MQ的重复消息投递问题。因此,对于消息最终一致性的方案,消息的消费者必须要对消息的消费支持幂等,不能造成同一条消息的重复消费的情况。

事务消息的缺点也很明显,即MQ存储了待发送的消息,如果出现了通讯或者其他的问题, 那么MQ无法感知到上游处理的最终结果。

那么事务消息RocketMQ是如何解决这个问题呢?它的解决方案非常的简单,就是其内部实现会有一个定时任务,去轮训状态为待发送的消息,然后给消息生产者发送检查请求,而消息生产者必须实现一个检查监听器,监听器的内容通常就是去检查与之对应的本地事务是否成功(一般就是查询DB),如果成功了,则MQ会将消息设置为可发送,否则就删除消息。

当然,我们自身也可以通过本地消息实现事务的最终一致性,但是实现的理念和我们现在了解的通过RocketMQ实现的思路是一样的。

五. GTS--分布式事务解决方案

大家可以去看看阿里巴巴在2017年在深圳峰会上出的一份报告:《破解世界性技术难题! GTS让分布式事务简单高效》,路径:jm.taobao.org/2017/04/13/…

这篇文章中,将GTS讲述的很清楚,我就不多说了。

小结

分布式事务,一直是开发人员常常要面对的问题,当前的GTS方案,也许是当今阶段最先进的方案之一。

但是技术一直在进步,也许在下一刻,会有着更多的更合适的方案,但是无论如何,核心思想都是为了保证数据的一致性,避免脏数据的出现。

今天,学习的分布式事务,非常复杂,单单只是粗浅的学习一下,感觉其中的内容也实在是太多太多,另外GTS因为从来没有接触过,没有做详细的解释,等到后面有机会,会单独给GTS写一篇博文。

接下来,可能不会在springCloud这个大的命题下写博文,会去了解一下java中的其他内容。另外,我对Python其实也有很大的兴趣,如果有机会的话,我会针对Python的学习过程来写一些博文。

就这样吧。

关注下面的标签,发现更多相似文章
评论