转账引发数据一致性思考

鉴于一个boy 发生支付宝提现事件,引发思考。支付宝提现成功,银行卡没查到到账。数据库不一致性问题··········

事务分为本地事务和分布式事务

本地事务

数据库事务四大特征:原子性(A),一致性(C),隔离性(I)和持久性(D),而在这四大特性中,一致性是最基本的特性,其它的三个特性都为了保证一致性而存在的

A账户给B账户转账100元(A、B处于同一个库中),如果A的账户发生扣款,B的账户却没有到账,这就出现了数据的不一致!为了保证数据的一致性,数据库的事务机制会让A账户扣款和B在账户到账的两个操作要么同时成功,如果有一个操作失败,则多个操作同时回滚,这就是事务的原子性,为了保证事务操作的原子性,就必须实现基于日志的REDO/UNDO机制。
但是,仅有原子性还不够,因为我们的系统是运行在多线程环境下,如果多个事务并行,即使保证了每一个事务的原子性,仍然会出现数据不一致的情况。
例如A账户原来有200元的余额, A账户给B账户转账100元,先读取A账户的余额,然后在这个值上减去100元,但是在这两个操作之间,A账户又给C账户转账100元,那么最后的结果应该是A减去了200元。但事实上,A账户给B账户最终完成转账后,A账户只减掉了100元,因为A账户向C账户转账减掉的100元被覆盖了!所以为了保证并发情况下的一致性,又引入的隔离性,即多个事务并发执行后的状态,和它们串行执行后的状态是等价的!隔离性又有多种隔离级别,为了实现隔离性(最终都是为了保证一致性)数据库又引入了悲观锁、乐观锁等等……本文的主题是分布式事务,所以本地事务就只是简单回顾一下,需要记住的一点是,事务是为了保证数据的一致性

事务隔离性理解:H1:当A转给B 100 之后,H2:A又转给C 100,但是在H2未提交之前,A查询到的余额还是H1,所以A此时余额只减少了100元,与实际应该减少200元,并发情况下数据不一致。
如下图是我自己的理解
在这里插入图片描述

分布式理论

学习一下别人的经历,警惕自己。
该经历来源 https://www.cnblogs.com/sujing/p/11006424.html
领导给我的第一个任务就是在列表上增加一个修改数据的功能。这能难倒我?我分分钟给你搞出来!不就是在列表上增加了一个“修改”按钮,点击按钮弹出框修改后保存就好了么。然而一切不像我想象的那么顺利,点击保存并刷新列表后,页面上的数据还是显示的修改之前的内容,像没有修改成功一样!过一会儿再刷新列表,数据就能正常显示了!测试多次之后都是这样!没见过什么大场面的我开始有点慌了,是我哪里写得不对么?最终,我不得不求助组内经验比较丰富的前辈!他深吸了一口气告诉我说:“毕竟是刚毕业的小伙子啊!我来跟你讲讲原因吧!我们的数据库是做了读写分离的,部分读库与写库在不同的网络分区。你的数据更新到了写库,而读数据的时候是从读库读取的。更新到写库的数据同步到读库是有一定的延迟的,也就是说读库与写库会有短暂的数据不一致”! “这样不会体验不好么?为什么不能做到写入的数据立马能读出来?那我这个功能该怎么实现呢?” 面对我的一堆问题,同事有些不耐烦的说:“听说过CAP理论吗?你先自己去了解一下吧”!

看了别人的经历,接下来属于我自己的探索了

1.CAP理论

CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)
在这里插入图片描述

好了,那如何理解CAP理论?
声明:链接:https://www.zhihu.com/question/54105974/answer/139037688
一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。

如何理解P?
假设有三个节点ABC,还有一个你的客户端节点D,原先D和ABC都在一个网络中,你的D可以访问ABC,某时刻C节点因为网线断了还是啥原因反正和ABD的网络断开了,ABD是一个网络分区,C自己独立为一个网络分区,此时你的D客户端就访问不到C节点的数据了,这就是分区容忍性差,如果C节点有一个复制节点C1,他没有和大部队ABD断开网络连接,那么此时分区容忍性就相较之前较好。

分布式事务

与本地事务不同的是,分布式事务需要保证的是分布式环境下,不同数据库表中的数据的一致性问题。分布式事务的解决方案有多种,如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银行等待接口响应会异常,误以为转账失败而回滚“第1步”操作,这也会出现了两个系统数据的不一致。
   对于问题的1、2都很好解决,如果对消息队列熟悉的朋友应该很快能想到可以引入消息中间件进行异步和削峰处理,于是又重新设计了一个方案,流程如下:
   >1、A银行对账户进行检查校验,进行金额扣减。
2、将对B银行的请求异步写入队列,主线程返回。
3、启动后台程序从队列获取待处理数据。
4、后台程序对B银行接口进行远程调用。
5、B银行对转入账户进行检查校验,进行金额增加。
6、B银行处理完成回调A银行接口通知处理结果。

在这里插入图片描述
  通过上面的图我们能看到,引入消息队列后,系统的复杂性瞬间提升了,虽然弥补了我们第一种方案的几个不足点,但也带来了更多的问题,比如消息队列系统本身的可用性、消息队列的延迟等等!并且,这样的设计依然没有解决我们面临的核心问题-数据的一致性!

1、如果“第1步”刚执行完,系统由于某种原因宕机了,那会导致A银行账户扣款了,但是写入消息队列失败,无法进行B银行接口调用,从而导致数据不一致。
2、如果B银行在执行“第5步”时由于校验失败而未能成功转账,在回调A银行接口通知回滚时网络异常或者宕机,会导致A银行转账无法完成回滚,从而导致数据不一致。

面对上述问题,我们不得不对系统再次进行升级改造。为了解决“A银行账户扣款了,但是写入消息队列失败”的问题,我们需要借助一个转账日志表,或者叫转账流水表,该表简单的设计如下:

在这里插入图片描述
  这个流水表需要怎么用呢?我们在“第1步”进行扣款时,同时往流水表写入一条操作流水,状态为“待处理”,并且这两个操作必须是原子的,也就是说必须通过本地事务保证这两个操作要么同时成功,要么同时失败!这就保证了只要转账扣款成功,必定会记录一条状态为“待处理”的转账流水。如果在这一步失败了,那自然就是转账失败,没有后续操作了。如果这步操作后系统宕机了导致没有将消息成功写入消息队列(也就是“第2步”)也没关系,因为我们的流水数据已经持久化了!这时候我们只需要加入一个后台线程进行补偿,定期的从转账流水表中读取状态为“待处理”且最后更新的时间距当前时间大于某个阈值的数据,重新放入消息队列进行补偿。这样,就保证了消息即使丢失,也会有补偿机制!B银行在处理完转账请求后会回调A银行的接口通知转账的状态,从而更新A银行流水表中的状态字段!这样就完美解决了上一个方案中的两个不足点。系统设计图如下:
在这里插入图片描述
  到目前为止,我们很好的解决了消息丢失的问题,保证了只要A银行转账操作成功,转账的请求就一定能发送到B银行!但是该方案又引入了一个问题,通过后台线程轮询将消息放入消息队列处理,同一次转账请求可能会出现多次放入消息队列而多次消费的情况,这样B银行会对同一转账多次处理导致数据出现不一致!那怎么保证B银行转账接口的幂等性呢?

同样的,我们可以在B银行系统中需要增加一个转账日志表,或者叫转账流水表,B银行每次接收到转账请求,在对账户进行操作的时候同时往转账日志表中插入一条转账日志记录,同样这两个操作也必须是原子的!在接收到转账请求后,首先根据唯一转账流水Id在日志表中查找判断该转账是否已经处理过,如果未处理过则进行处理,否则直接回调返回! 最终的架构图如下:
   在这里插入图片描述
所以,我们这里最核心的就是A银行通过本地事务保证日志记录+后台线程轮询保证消息不丢失。B银行通过本地事务保证日志记录从而保证消息不重复消费!B银行在回调A银行的接口时会通知处理结果,如果转账失败,A银行会根据处理结果进行回滚。

本文综合csdn文章、知乎、百度学习与理解,进行总结。欢迎小伙伴一起学习!

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZrZrZr.l

您的打赏是我的鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值