【分布式】聊一聊分布式事务

一、前言

       在分布式、微服务大行其道的今天,相信大家对这些词都不陌生。拆分微服务的好处如下:

       1、每个人只需要维护自己单独的服务,没有了以前的各种代码冲突

       2、自己想测试、想发布、想升级,只需要care自己写的代码就ok,很方便,很贴心。

       任何技术的引用,都会带来新的问题。今天就谈一谈分布式系统中最棘手的问题:分布式事务。  

二、神马是事务

       事务就是一系列操作,要么同时成功,要么同时失败。然后从事务的ACID展开叙述。那么为啥要有这个概念呢?是为了解决什么问题?

       最经典的例子,莫过于银行转账。我们要从A账户转1000到B账户。正常情况下,我们从A转出1000到B账户之后,A账户余额减1000(这个操作,我们用action1代表),B账户余额加1000(这个操作,我们用action2代表)。action1和action2是两个操作。既然是两个操作就一定会存在执行的先后顺序。那么可能会出现action1执行完,刚准备去执行action2的时候出问题了。(比如数据库负载过大,暂时拒绝访问)。类比到生活中就是,我给朋友转了1000块,然后我卡里的余额少了1000,但是我朋友没收到钱。

       为了解决“钱到底去哪里了”的问题,引入了事务的概念。也就是说,既然我转账的时候你保证不了100%能成功,比如银行系统只能保证99.99%的高可用,那么在那0.01%的时间里如果出现了上述问题,银行系统直接回滚action1操作?(即把1000块钱再加回余额中去)

       对于银行系统来说,可能在0.01%的时间里我保证不了action1和action2同时成功,那么在出问题的时候,我保证它俩同时失败。(事务的原子性)

三、java中的事务

       搞懂了事务是怎么来的,我们来看点眼熟的,java中的事务是怎么玩的?

       java中,我们平时用的最多的就是在service层的增删改查方法上添加@Transactional注解。让spring去帮我们管理事务。

       它的底层会给我们的service组件生成一个对应的proxy动态代理,这样所有对service组件的方法都由它对应的proxy来接管。

       当proxy在调用对应业务方法比如add()时,proxy就会基于AOP的思想在调用真正的业务方法前执行setAutoCommit(false)打开事务。

然后在业务方法执行完后执行commit提交事务,当在执行业务方法的过程中发生异常时就会执行rollback来回滚事务。

当然@Transactional注解具体的实现细节这里不再展开,这个不是本篇文章的重点,本文的topic是“分布式事务”,关于@Transactional注解大家有兴趣的话,可以自己研究下。

四、分布式事务

       终于来到了重点。既然有了事务,并且使用spring的@Transactional注解来控制事务是如此的方便。那为啥还要搞一个分布式事务的概念出来呢?更近一步,分布式事务和普通事务到底是啥关系?有什么区别?分布式事务又是为了解决什么问题出现的?

简单来说,就是分布式系统中的事务,至于什么是分布式系统,可以参考石衫老师的文章。《到底啥是分布式系统开发经验?》

               

       如上图所示,一个单块系统有三个模块:员工模块、财务模块、请假模块。假如现在有1个操作,需要按顺序去调用完成这三个模块中的接口。这个操作是一个整体,包含在1个事务中,要么同时成功,要么同时失败回滚。这是ok的。但当我们把单块系统拆分成分布式系统或者微服务架构,事务就不是上面那么玩了。

       拆分成分布式系统之后的图:

               

上图是同一操作在分布式系统中的执行情况。 员工模块、财务模块、请假模块分别拆分成员工系统、财务系统、请假系统。

       比如一个用户进行一个操作,这个操作需要先调用员工系统预先处理一下,然后http或者rpc的方式分别调用财务系统和请假系统的接口做进一步的处理。它们的操作都需要落地到数据库中。这3个系统的一系列操作是一个分布式事务,此时这三个系统的操作,要么同时成功,要么同时失败。@Transactional注解是不行的,因为它只能控制同一个JVM进程中的事务,但是对于这种跨多个JVM进程的事务无能无力。

五、分布式事务的几种实现思想

       下面给大家介绍几种常见的分布式事务实现方案。

1、可靠消息最终一致性方案

       整个流程图如下所示:

              

我们来解析一下这个方案的大致流程:

① A系统先发一个prepared消息,如果消费发送失败,直接取消操作,后续都不执行了。

② 如果这个消息发送成功了,执行A系统的本地事务,失败就告诉mq回滚消息,后序都不执行了。

③ 如果A系统本地事务执行成功,告诉mq发送确认消息

④ 那如果A系统迟迟不发确认消息呢?

     此时mq会自动轮训所有prepared消息,然后调用A系统事先提供的接口,通过这个接口,反查A系统的上次本地事务是否执行成功。如果成功就发送确认消息给MQ,失败则告诉mq回滚消息。

⑤ 此时B系统接收到确认消息,然后执行本地事务,如果本地事务执行成功则事务正常完成。

⑥ 如果系统B的本地事务执行失败了咋办?

     基于MQ重试,MQ会自动不断重试直到成功,实在不行,可以发送报警由人工来手动回滚或补偿。 

 

       这种方案的要点就是 可以基于MQ来不断进行重试,最终一定会执行成功。因为一般执行执行失败的原因为网络抖动,或者数据库瞬间负载过高。都是暂时性问题。通过这种方案99.99%的情况都是可以保证数据最终一致的,剩下的0.1%出问题的时候,就人工修复数据。

       适用场景:

       这个方案的使用还是比较广的,目前国内互联网公司大都是基于这个思路玩的。

2、最大努力通知方案 

     整个流程如如下所示:

            

这个方案的大致流程 :

① 系统A本地事务执行完之后,发送个消息到MQ

② 这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ,然后写入数据库中记录下来,或者是放入个内存队列。接着调用系统B的接口

③ 假如系统B执行成功就万事ok了,但是如果系统B执行失败了呢?

那么此时最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃。

这套方案和上面的可靠消息最终一致性方案的区别

       可靠消息最终一致性方案可以保证的是只要系统A的事务完成,通过不断(无限次)重试来保证系统B的事务总会完成。

       但最大努力方案就不同,如果系统B本地事务执行失败了,那么它会重试N次之后就不再重试,系统B的本地事务可能就不会完成啦。至于你想控制它究竟有多“努力”,这个需要结合自己的业务来配置。

       比如在电商系统中,在下完订单后发短信通知用户下单成功的业务场景中,下单正常完成,但是到了发短信的这个环节,由于短信服务暂时有点问题,导致重试了3次还是失败。那么此时就不再重试发送短信了。因为在这个场景中我们认为3次就已经算是尽了“最大努力”了。

       简单总结:就是在指定的重试次数内,如果能执行成功那么皆大欢喜,如果超过了最大重试次数就放弃,不再进行重试。

       适用场景:

       一般用在不太重要的业务场景中,就是那种完成的话是锦上添花,但失败的话对我也没啥坏影响。比如上面提到的电商中的部分通知短信,就比较适合使用这种最大努力通知方案来做分布式事务的保证。

3、TCC强一致性方案

       TCC的全称是:

       Try(尝试)

       Confirm(确认/提交)

       Cancel(回滚) 

       这个其实是用到了补偿的概念,分为3个阶段。

       ① Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。

       ② Confirm阶段:这个阶段说的是在各个系统中实际执行的操作。

       ③ Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。

       给大家举个例子:

                   

比如说跨银行转账的时候,要涉及两个银行的分布式事务,如果用TCC方案来实现,思路是这样的。

1、Try阶段:先把两个银行中的资金给它冻住就不让操作啦。

2、confirm阶段:执行实际的转账操作,A银行账户资金减扣,B银行账户的资金增加。

3、Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去  

适用场景:

       这种方案说实话几乎很少有人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。比如说我们,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,在资金上不允许出现问题。

       比较适合的场景:除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了。你需要自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码。而且最好是你的各个业务执行的时间都比较短。但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。

六、学员提问的问题

       我们想保证某个系统非常的可靠,任何一个数据都不能错。我们用的是微服务架构,几十个服务。结果,我们一盘点,发现,如果到处都要搞的话,一个系统要做几十个分布式事务出来。

       我们的经验,我带几十个人的team,最大的一个项目,起码几百个服务。复杂的分布式大型系统,里面其实也没几个分布式事务。

       你其实用任何一个分布式事务方案,都会导致你那块代码复杂10倍。很多请求下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务

       如果调用出错,就打印异常日志。每个月也就那么几个bug,很多bug是功能性的、体验性的。真正涉及到数据层面的一些bug,一个月可能就两三个。

       需要balance一下。如果你为了确保系统保证数据100% 不出错,上了几十个分布式事务。代码太复杂,性能太差,系统吞吐量、 性能大幅度下跌。

       99%的分布式接口调用,不要做分布式事务。直接就是监控(发邮件,发短信)、记录日志(一旦出错,完整的日志),事后快速定位,排查和出解决方案,修复数据。

       每个月,每隔几个月,都会对少量因为bug导致出错的数据,进行人工的数据修复。自己临时动手写个程序。可能要补一下数据,要删一下数据。或者修改一些字段的值。

       比你做50个事务,成本要低得多。

       trade off,权衡。要用分布式事务,一定会有成本的,代码会很复杂,开发很长时间,性能和吞吐量下降,系统更加复杂、更加脆弱反而更容易出bug。好处就是如果做好了,TCC,可靠消息最终一致性方案,一定可以100%保证你那块的数据不会出错。

       1%,0.1%,0.01%的业务,资金、交易、订单,我们会用分布式事务方案来保证,会员积分、优惠券、商品信息,其实不要这么搞了。

六、总结

       本篇介绍了什么是分布式事务,然后还介绍了最常用的3种分布式事务方案。但除了上边的方案外,其实还有两阶段提交方案(XA方案)和本地消息表等方案。但是说实话极少有公司使用这些方案,鉴于篇幅所限,不做介绍。后续如果有机会再出篇文章,详细聊聊这两种方案的思路。

 

 

           

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值