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

给女朋友的一次转账,让我明白了“分布式事务”

时间:2023-03-18 17:16:14 科技观察

前两天发了工资,第一反应就是给远在天边的女朋友一个惊喜!于是我打开了平安银行的APP给女朋友转账!填写对方招商银行卡的卡??号和账户名,一键转账!完毕!图片来自宝途网。提示界面显示:“处理中,等待对方银行返回结果……”。出色地!毕竟是跨行转账,等几秒很正常啊!脑海里开始浮现女友收到转账后惊喜和感动的画面!然而,一切并没有那么顺利。没过一会,app出现图2提示转账失败,因为收款人的账户名不匹配!!!刚才已经扣了卡里的钱,现在提示转账失败。银行会吞掉我的钱吗?转账失败的钱可以退给我吗?就在我紧张、焦虑、坐立不安的时候,我收到了一条消息,说应用程序已被更正。刚才转账失败的钱已经退给我了。看来是我多虑了……这也证明了平安银行的app还是比较安全可靠的!为什么我的卡扣钱这么快,对方却要过几秒才到?而且转账失败后,扣的钱能及时退回我卡里的账户吗?退钱不成功怎么办?或者我转账一次,对方收到两次转账申请怎么办?带着这些疑问,“交易”这个词浮现在我的脑海!在我们“唠叨”的时候,老师往往会通过转账的方式给我们讲解事务,但是和这里场景不同的是,老师讲的是本地事务,而我们这里处理的是分布式事务!让我们先简单回顾一下本地交易吧!说到本地事务,大家可能都不陌生,因为这是数据库引擎可以支持的!所以也叫数据库事务。数据库事务的四个特性是:原子性(A)一致性(C)隔离性(I)持久性(D)在这四个特性中,我认为一致性是最基本的特性,其他三个特性的存在就是为了保证一致性!回到学生时代老师给我们举个经典的例子,A??账户转100元到B账户(A和B在同一个数据库),如果A的账户被扣了,B的账户没有到账,这会造成数据不一致!为了保证数据的一致性,数据库的事务机制允许A账户扣款和B账户扣款两个操作同时到达同时成功。如果一个操作失败,多个操作将同时回滚。这就是事务的原子性。为了保证事务操作的原子性,必须实现基于日志的REDO/UNDO机制!但是光有原子性是不够的,因为我们的系统是运行在多线程环境下的。如果多个Transaction是并行的,即使保证了每个事务的原子性,仍然会出现数据不一致的情况。比如A账户原本有200元余额,A账户转100元给B账户,先读取A账户余额,然后从这个值中减去100元,但是在这两次操作之间,A账户给了AccountC转100元,那么***结果应该是A减200元。但实际上,A账户到B账户的转账最终完成后,A账户只扣了100元,因为A账户到C账户的转账减去的100元被覆盖了!所以为了保证并发情况下的一致性,又引入了隔离,即多个事务并发执行后的状态,等同于它们串行执行后的状态!隔离有多个隔离级别,为了实现隔离(最终保证一致性),数据库引入了悲观锁、乐观锁等。本文的主题是分布式事务,所以本地事务只是简单回顾一下。有一点要记住,事务是为了保证数据的一致性!分布式理论还记得刚毕业的时候,怀着满腔的热情去了一家互联网公司。领导交给我的第一个任务就是给列表增加一个修改数据的功能。这能难倒我吗?我会在几分钟内为您解决!不就是在列表中添加一个“修改”按钮,点击按钮弹出框,修改并保存。然而,一切并没有想象中那么顺利。点击保存刷新列表后,页面数据显示的还是修改前的内容,好像修改不成功!过一会刷新列表,数据就可以正常显示了!多次测试第一次之后就是这样!没见过什么大场面的我开始慌了。我写错了吗?最后只好求助群里比较有经验的前辈了!他深吸一口气告诉我:“毕竟是个刚毕业的小伙子!我来告诉你原因吧!我们的数据库是读写分离的,有的读写数据库在不同的网络分区.你的数据已经更新到写库中了,读数据的时候,是从读库中读取的。将写库更新的数据同步到读库是有一定延迟的,也就是说读库和写库之间会出现短暂的数据不一致“!”这样的体验会不会很糟糕?为什么写入的数据不能立即读取?那我要怎么实现这个功能呢?”面对我的一堆问题,同事不耐烦地说:“你听说过CAP理论吗?你自己先去了解一下”!于是我开始查阅各种资料,了解这个奇怪单词背后的秘密!CAP理论是由美国加州大学的EricBrewer教授提出的。这个理论告诉我们分布式系统不能同时满足一致性和可用性。分区容忍度(Partitiontolerance)的三个基本要求只能同时满足其中两个。一致性:这里的一致性是指数据的强一致性,也称为线性一致性,是指分布式环境下的数据。其中,数据是否可以在多副本之间保持一致。也就是说,如果在数据写入操作后立即进行读取操作,则刚写入的值必须能够读取。(任何在写操作完成后开始的读操作都必须返回那个值,或者是后面写操作的结果)可用性:非故障节点收到的任何请求必须能够在有限的时间内响应结果。(系统中非故障节点收到的每个请求都必须产生响应)分区容错:如果集群中的机器被分成两部分,两部分之间不能相互通信,系统是否可以继续正常工作。(网络将被允许丢失任意多条从一个节点发送到另一个节点的消息)在分布式系统中,分区容错性基本得到保证。也就是说,只有一致性和可用性之间的权衡。一致性和可用性,为什么不能同时建立呢?回到前面修改列表的例子,由于数据会分布在不同的网络分区,必然会出现数据同步的问题,而同步会出现网络延迟、异常等问题,所以会出现数据不一致的情况!如果要保证数据的一致性,那么在对写库进行操作的时候,就必须要对其他读库操作加锁。只有写入成功并完成数据同步后,才能再次释放读写,系统在锁定期间失去可用性。更详细的CAP理论可以参考这篇文章,比较通俗易懂!分布式事务分布式事务需要满足分布式场景下事务的需求!上一篇我们讲了消息中间件,那么本文要讲的就是分布式事务。结合两者,我们就有了基于消息中间件的分布式事务解决方案!不管是本地事务还是分布式事务,都是为了数据处理的一致性!一致性这个词之前已经提过很多次了!与本地事务不同,分布式事务需要保证分布式环境下不同数据库表中数据的一致性。分布式事务的解决方案有很多,比如XA协议、TCC三阶段提交、基于消息队列等,本文将只介绍基于消息队列的解决方案!本地事务谈的是一致性,而分布式事务难免面临一致性问题!回到最初跨行转账的例子,如果A银行用户向B银行用户转账,正常的流程应该是:A银行核对转出账户并扣款。A银行同步调用B银行的转账接口,B银行对转账账户进行核对并增加金额。B银行将处理结果返回给A银行,在一般情况下对一致性要求不高的场景下,这样的设计是可以满足要求的。但是如果这样实施,像银行这样的系统可能会破产。我们来看看这种设计的主要问题:同步调用远程接口,如果接口耗时,会导致主线程长时间阻塞。不能很好地控制流量,A银行系统的峰值流量可能压垮B银行系统(当然B银行肯定会有自己的限流机制)。如果刚刚执行完“步骤1”,系统因为某种原因宕机,会导致A银行扣钱,而B银行还没有收到接口调用,会造成两个系统数据不一致。如果在执行“步骤3”后,B银行因为某种原因宕机,无法正确响应请求(实际上转账操作已经在B银行的系统中执行并入库),那么A银行就会出现异常在等待接口响应时。误以为传输失败而回滚“step1”操作,这样也会造成两个系统数据不一致。问题1和2都很容易解决。熟悉消息队列的朋友应该很快想到引入消息中间件进行异步和调峰处理。于是重新设计了一个方案,流程如下:A银行核对账户并扣款。将对BankB的请求异步写入队列,主线程返回。启动守护进程以从队列中获取挂起的数据。后台程序远程调用B银行接口,B银行对转账账户进行核对并增加金额。B银行处理完毕,回调A银行接口通知处理结果。从上图可以看出,引入消息队列后,系统的复杂度瞬间增加。虽然弥补了我们第一种方案的几个不足,但也带来了更多的问题。比如消息队列系统本身的可用性,消息队列的延迟等等!而且这种设计还是没有解决我们面临的核心问题,数据一致性!①如果刚执行完“步骤1”,系统由于某种原因宕机,会导致A银行账户扣钱,但是写入消息队列失败,无法调用B银行接口,导致数据不一致。②如果B银行在执行“步骤5”过程中验证失败导致转账失败,回调A银行接口通知回滚时网络异常或宕机,将导致A银行转账无法完成回滚,导致数据不一致。面对以上问题,我们不得不再次升级系统。为了解决“某银行账户有扣款,但是写入消息队列失败”的问题,我们需要用到转账日志表,或者叫转账流表。表的简单设计如下:这个流表怎么用?我们在“第1步”进行推算的时候,同时向流表写入了一个操作流,状态为“pending”。而且这两个操作必须是原子的,也就是说必须通过本地事务来保证这两个操作同时成功或者同时失败!这样可以保证只要转账扣费成功,就会记录一个状态为“pending”的转账流程。如果在这一步失败了,自然是转账失败,也就没有后续的操作了。如果在这一步之后系统宕机,消息没有成功写入消息队列(也就是“第2步”)也没有关系,因为我们的管道数据已经被持久化了!这时我们只需要增加一个后台线程来处理Compensation,定时从转流表中读取状态为“pending”且最新更新时间距离当前时间大于一定阈值的数据,并放回去进入消息队列进行补偿。这样就保证了即使消息丢失了,也有补偿机制!B银行处理完转账请求后,会回调A银行的接口通知转账状态,从而更新A银行流表中的status字段!这样,***就解决了之前方案的两个不足。系统设计图如下:至此,我们已经很好的解决了消息丢失的问题,保证只要A银行的转账操作成功,就会向B银行发送转账请求!但是这种方案引入了另外一个问题,通过后台线程轮询,将消息放入消息队列中进行处理。同一个传输请求可能会多次放入消息队列并被多次消费。这样B银行就会多次处理同一个转账,造成数据不一致!如何保证B银行转账接口的幂等性?同样,我们需要在B银行系统中增加一个转账日志表,或者说转账流水表。银行B每次收到转账请求,在对账户进行操作时,同时在转账日志表中插入一条转账日志记录。同样,这两个操作也必须是原子的!收到转账请求后,首先根据唯一的转账交易ID在日志表中查找,判断转账是否已经处理。如果还没有处理,则进行处理,否则直接回调!最终的架构图如下:所以,这里的核心是A银行通过本地事务+后台线程轮询来保证日志记录,保证消息不丢失。B银行通过本地事务保证日志记录,保证消息不被重复消费!B银行会在回调A银行接口时通知处理结果,如果转账失败,A银行会根据处理结果回滚。当然,分布式事务最好的解决方案就是尽量避免分布式事务!作者:苏静简介:多年大型互联网项目开发经验,对高并发、分布式、微服务技术有深入研究及相关实践经验。经验丰富的自学,热衷于技术研究和分享!座右铭:时刻保持虚心的学习态度!