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