当前位置: 首页 > 科技观察

“分布式事务”,这次我彻底明白了!

时间:2023-03-12 06:52:15 科技观察

在分布式和微服务的今天,相信大家对这些名词都不陌生。而说到使用分布式,或者拆分微服务的好处,你肯定能想到很多。图片来自宝途网例如大家只需要维护自己独立的服务,没有之前的代码冲突。如果要测试、发布、升级,只需要Care写的代码,非常方便贴心!然而,任何事情都有两个方面,也会带来一些问题。今天的文章是关于分布式系统的。架构带来的棘手问题之一:分布式事务!什么是交易?首先提出一个问题:什么是交易?有人会说事务是一系列操作,要么同时成功,要么同时失败;描述了ACID属性(原子性、一致性、隔离性和持久性)。的确,事务是为了保证一系列操作能够正常进行,必须同时满足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元)对于银行系统,我可能无法保证Action1和Action2在0.01%的时间内同时成功,所以出问题的时候,我保证他们两个都会同时失败。(事务的原子性)通过这个例子,一开始提出的两个问题得到了解答(为什么要有事务?事务要解决什么问题?)总结一下:事务保证一系列操作可以安全正确的进行任何情况。Java中的事务搞清楚了事务,我们再来看看熟悉的事务。Java中的事务是如何工作的?在Java中,我们通常会在Service层的增删改查方法上使用@Transactional注解,让SpringGo帮我们管理我们的事务。它的底层会为我们的Service组件生成一个对应的Proxy动态代理,这样Service组件的所有方法都会被它对应的Proxy接管。当Proxy在调用add()等相应的业务方法时,Proxy会根据AOP的思想执行setAutoCommit(false)开启事务,然后再调用真正的业务方法。然后在业务方法执行后执行Commit提交事务,在业务方法执行过程中出现异常时执行Rollback回滚事务。当然@Transactional注解的具体实现细节这里就不展开了。这不是本文的重点。本文的主题是“分布式事务”。如果对@Transactional注解感兴趣,可以自己打断Debug源码研究。真正的知识来自源代码。什么又是分布式事务?铺垫了半天,终于到了本文的第一点!首先大家有没有想过:既然有了事务,而且用Spring的@Transactional注解来控制事务那么方便,那为什么还要创建分布式事务的概念呢?更进一步,分布式事务和普通事务有什么关系?有什么不同?分布式事务似乎解决了哪些问题?各种问题接踵而至,不用着急,有了这些想法,接下来我们就来详细说说分布式事务。既然叫分布式事务,那肯定跟分布有关!简单的说,分布式事务就是指分布式系统中的事务。好了,我们继续,我们先来看下图:如上图所示,一个单块系统有3个模块:员工模块、财务模块和请假模块。我们现在有一个操作需要调用以完成这3个模块中的接口。这个操作是一个整体,包含在一个事务中,要么同时成功,要么同时失败回滚。不成功就会成功,这个没有问题。但是当我们把单体系统拆分成分布式系统或者微服务架构的时候,事务就没有上面那么好玩了。首先我们来看一下拆分成分布式系统后的架构图,如下图:上图是在分布式系统中执行相同的操作。员工模块、财务模块和请假模块分别分为员工系统、财务系统和请假系统。例如,用户执行一个操作,需要通过调用员工系统对操作进行预处理,然后通过HTTP或者RPC调用财务系统和请假系统的接口做进一步的处理。它们的操作需要分别在数据库中实现。这三个系统的一系列操作其实都需要包裹在同一个分布式事务中。此时,这三个系统的操作要么同时成功,要么同时失败。在分布式系统中完成一个操作通常需要多个系统之间的协调调用和通信,比如上面的例子。员工系统、财务系统、请假系统这三个子系统通过HTTP或RPC进行通信,而不是单块系统中不同模块之间的调用。这就是分布式系统和单块系统***的区别。一些平时不太关注分布式架构的同学看到这里可能会说:我用Spring的@Transactional注解就可以了,何必费那么大劲呢!但是这里有一个极其重要的一点:单体系统运行在同一个JVM进程中,而分布式系统中的每个系统都运行在自己的JVM进程中。所以不能直接加上@Transactional注解,因为它只能控制同一个JVM进程中的事务,而对于这种跨越多个JVM进程的事务就无能为力了。分布式事务的几种实现思路弄清楚什么是分布式事务之后,分布式事务是如何工作的呢?下面介绍几种分布式事务的实现。可靠消息最终一致性方案的整个流程图如下:下面来解释一下这个方案的大致流程:系统首先向MQ发送一个Prepared消息,如果Prepared消息发送失败,则直接取消操作,做notexecute后续操作不再执行。如果消息发送成功,则执行系统A的本地事务,如果执行失败,则告诉MQ回滚消息,后续操作将不再执行。如果系统A的本地事务执行成功,则告诉MQ发送确认消息。如果A系统长时间不发送确认消息怎么办?此时MQ会自动定时轮询所有Prepared消息,然后提前调用系统A提供的接口,通过该接口查看系统A的最后一个本地事务是否执行成功。如果成功,向MQ发送确认消息;如果失败,则告诉MQ回滚消息。(后面的操作不再执行)此时B系统会收到确认消息,然后执行本地事务。如果本地事务执行成功,则事务正常完成。B系统本地事务执行失败怎么办?基于MQ重试,MQ会自动不断重试,直到成功。如果失败,可以发送警报以手动回滚和补偿。这个方案的要点是可以基于MQ不断重试,最终会执行成功。因为执行失败的一般原因是网络抖动或者数据库瞬时负载过高,都是暂时性的问题。通过该方案,99.9%的情况下可以保证数据的最终一致性,当剩下0.1%的问题出现时,可以人工修复数据。适用场景:该方案应用还是比较广泛的。目前国内大部分互联网公司都是基于这种思路。***努力通知方案的整个流程图如下:本方案的大致流程:系统A的本地事务执行完毕后,向MQ发送消息。将有一个专门使用MQ的工作量通知服务。这个服务会消费MQ,然后写入数据库记录,或者放入内存队列。然后调用系统B的接口,如果系统B执行成功,一切OK,但是如果系统B失败了怎么办?然后这个时候,***就努力去通知服务,定期尝试再次调用系统B,重复N次,最后还是不行就放弃了。该方案与上述可靠消息最终一致性方案的区别:可靠消息最终一致性方案可以保证只要系统A的事务完成,系统B的事务总量就可以通过不间断的(***次)重试。将会完成。但尽力而为是不同的。如果系统B的本地事务执行失败,会重试N次,不会再重试,系统B的本地事务可能没有完成。至于你想控制多难,这个需要结合自己的业务来配置。例如,对于电商系统,在下单后发送短信通知用户下单成功的业务场景下,订单正常完成,但是到了发送短信的时候消息,由于短信服务暂时出现问题,重试3次仍失败。然后此时不再尝试发送短信,因为在这种情况下我们认为3次被认为是“尽力而为”。小结:在规定的重试次数内,执行成功,皆大欢喜。如果重试次数超过最大重试次数,则放弃,不再重试。适用场景:一般用在不太重要的业务操作上,也就是那种完成了就锦上添花,失败了对我也没有什么不好影响的场景。比如上面提到的电子商务中的一些通知消息,就更适合使用这种勤劳的通知方案来保证分布式事务。TCC强一致性方案TCC的全称是:Try(尝试)Confirm(确认/提交)Cancel(回滚)这个其实用到了补偿的概念,分为三个阶段:Try阶段:这个阶段是针对每一个的检测服务的资源并锁定或保留资源。Confirm阶段:这个阶段是指在每个服务中进行实际操作。取消阶段:如果任何服务业务方法执行失败,那么这里需要进行补偿,也就是对已经执行成功的业务逻辑执行回滚操作。我举个例子:比如跨行转账,涉及到两家银行的分布式交易。如果采用TCC方案来实现,思路如下:尝试阶段:先将两个银行账户中的资金转入冻结,不允许操作。确认阶段:执行实际转账操作,扣除A银行账户资金,增加B银行账户资金。取消阶段:如果任何银行操作失败,那么需要回滚补偿,即如果A银行账户已经扣款,但是B银行账户未能增加资金,那么A银行账户资金必须添加。回去。适用场景:这个方案说实话很少有人用,我们用的也比较少,但是也有用到的场景。因为这个事务回滚其实非常依赖你自己的代码来回滚补偿,会造成巨大的补偿代码,非常恶心。比如我们一般在跟钱相关的场景,处理钱,支付,交易,严格保证分布式交易要么全部成功,要么自动回滚,严格保证资金的正确性,不允许出现问题资金。更适合的场景:除非你真的对一致性要求太高,是你系统的核心,比如资金的常见场景,那么你可以使用TCC方案。需要自己写很多业务逻辑,判断一个事务中的各个环节是否OK,不OK则执行补偿/回滚代码。而且归根结底就是你的各项业务的执行时间比较短。不过说实话,一般尽量不要这样做。自己写回滚逻辑或者补偿逻辑真的很恶心,而且业务代码很难维护。小结本文先介绍什么是分布式事务,然后介绍最常用的三种分布式事务方案。不过除了上述方案外,其实还有两阶段提交方案(XA方案)和本地消息表。不过说实话,很少有公司使用这些解决方案,限于篇幅就不做介绍了。如果以后有机会再发一篇文章,我会详细说说这两种方案的思路。中华石山:十余年BAT架构经验,一线互联网公司技术总监。带领数百人团队开发过亿级大流量高并发系统。多年工作积累的研究手稿和经验总结,现整理成文,一一传授。微信公众号:石山的建筑笔记(ID:shishan100)。