在咱们刚开始接触学习事务的时候,一般会经过本地事务的例子来理解,但在实际生产场景中,咱们面对的更可能是分布式事务!咱们先来简单回顾一下本地事务!node
谈到本地事务,你们可能都很熟悉,由于这个数据库引擎层面能支持的!因此也称数据库事务,数据库事务四大特征:git
而在这四大特性中,我认为一致性是最基本的特性,其它的三个特性都为了保证一致性而存在的!github
回到学生时代老师给咱们举的经典栗子,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元被覆盖了!因此为了保证并发状况下的一致性,又引入的隔离性,即多个事务并发执行后的状态,和它们串行执行后的状态是等价的!隔离性又有多种隔离级别,为了实现隔离性(最终都是为了保证一致性)数据库又引入了悲观锁、乐观锁等等……本文的主题是分布式事务,因此本地事务就只是简单回顾一下,须要记住的一点是,事务是为了保证数据的一致性!数据库
还记得刚毕业那年,领导给个人一个任务就是在列表上增长一个修改数据的功能。这能难倒我?我分分钟给你搞出来!不就是在列表上增长了一个“修改”按钮,点击按钮弹出框修改后保存就行了么。然而一切不像我想象的那么顺利,点击保存并刷新列表后,页面上的数据仍是显示的修改以前的内容,像没有修改为功同样!过一下子再刷新列表,数据就能正常显示了!测试屡次以后都是这样!没见过什么大场面的我开始有点慌了,是我哪里写得不对么?最终,我不得不求助组内经验比较丰富的前辈!他深吸了一口气告诉我说:“毕竟是刚毕业的小伙子啊!我来跟你讲讲缘由吧!咱们的数据库是作了读写分离的,部分读库与写库在不一样的网络分区。你的数据更新到了写库,而读数据的时候是从读库读取的。更新到写库的数据同步到读库是有必定的延迟的,也就是说读库与写库会有短暂的数据不一致”! “这样不会体验很差么?为何不能作到写入的数据立马能读出来?那我这个功能该怎么实现呢?” 面对个人一堆问题,同事有些不耐烦的说:“据说过CAP理论吗?你先本身去了解一下吧”!是我开始查阅各类资料去了解这个陌生的词背后的秘密!编程
CAP理论是由加州大学Eric Brewer教授提出来的,这个理论告诉咱们,一个分布式系统不可能同时知足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三个基本需求,最多只能同时知足其中两项。
一致性:这里的一致性是指数据的强一致,也称为线性一致性。是指在分布式环境中,数据在多个副本之间是否可以保持一致的特性。也就是说对某个数据进行写操做后立马执行读操做,必须能读取到刚刚写入的值。(any read operation that begins after a write operation completes must return that value, or the result of a later write operation)
可用性:任意被无端障节点接收到的请求,必须可以在有限的时间内响应结果。(every request received by a non-failing node in the system must result in a response)
分区容错性:若是集群中的机器被分红了两部分,这两部分不能互相通讯,系统是否能继续正常工做。(the network will be allowed to lose arbitrarily many messages sent from one node to another)网络
在分布式系统中,分区容错性是基本要保证的。也就是说只能在一致性和可用性之间进行取舍。一致性和可用性,为何不可能同时成立?回到以前修改列表的例子,因为数据会分布在不一样的网络分区,必然会存在数据同步的问题,而同步会存在网络延迟、异常等问题,因此会出现数据的不一致!若是要保证数据的一致性,那么就必须在对写库进行操做时,锁定其余读库的操做。只有写入成功且完成数据同步后,才能从新放开读写,而这样在锁按期间,系统丧失了可用性。更详细关于CAP理论能够参考这篇文章,该文章讲得比较通俗易懂!多线程
分布式事务就是在分布式的场景下,须要知足事务的需求!上篇文章咱们聊过了消息中间件,那这篇文章咱们要聊的是分布式事务,把二者一结合,便有了基于消息中间件的分布式事务解决方案!无论是本地事务,仍是分布式事务,都是为了解决数据的一致性问题!一致性这个词我们前面屡次说起!与本地事务不一样的是,分布式事务须要保证的是分布式环境下,不一样数据库表中的数据的一致性问题。分布式事务的解决方案有多种,如XA协议、TCC三阶段提交、基于消息队列等等,本文只会涉及基于消息队列的解决方案!架构
本地事务讲到了一致性,分布式事务不可避免的面临着一致性的问题!回到最开始跨行转帐的例子,若是A银行用户向B银行用户转帐,正常流程应该是:并发
一、A银行对转出帐户执行检查校验,进行金额扣减。
二、A银行同步调用B银行转帐接口。
三、B银行对转入帐户进行检查校验,进行金额增长。
四、B银行返回处理结果给A银行。app
在正常状况对一致性要求不高的场景,这样的设计是能够知足需求的。可是像银行这样的系统,若是这样实现大概早就破产了吧。咱们先看看这样的设计最主要的问题:
一、同步调用远程接口,若是接口比较耗时,会致使主线程阻塞时间较长。
二、流量不能很好控制,A银行系统的流量高峰可能压垮B银行系统(固然B银行确定会有本身的限流机制)。
三、若是“第1步”刚执行完,系统因为某种缘由宕机了,那会致使A银行帐户扣款了,可是B银行没有收到接口的调用,这就出现了两个系统数据的不一致。
四、若是在执行“第3步”后,B银行因为某种缘由宕机了而没法正确回应请求(实际上转帐操做在B银行系统已经执行且入库),这时候A银行等待接口响应会异常,误觉得转帐失败而回滚“第1步”操做,这也会出现了两个系统数据的不一致。
对于问题的一、2都很好解决,若是对消息队列熟悉的朋友应该很快能想到能够引入消息中间件进行异步和削峰处理,因而又从新设计了一个方案,流程以下:
一、A银行对帐户进行检查校验,进行金额扣减。
二、将对B银行的请求异步写入队列,主线程返回。
三、启动后台程序从队列获取待处理数据。
四、后台程序对B银行接口进行远程调用。
五、B银行对转入帐户进行检查校验,进行金额增长。
六、B银行处理完成回调A银行接口通知处理结果。
经过上面的图咱们能看到,引入消息队列后,系统的复杂性瞬间提高了,虽然弥补了咱们第一种方案的几个不足点,但也带来了更多的问题,好比消息队列系统自己的可用性、消息队列的延迟等等!而且,这样的设计依然没有解决咱们面临的核心问题-数据的一致性!
一、若是“第1步”刚执行完,系统因为某种缘由宕机了,那会致使A银行帐户扣款了,可是写入消息队列失败,没法进行B银行接口调用,从而致使数据不一致。
二、若是B银行在执行“第5步”时因为校验失败而未能成功转帐,在回调A银行接口通知回滚时网络异常或者宕机,会致使A银行转帐没法完成回滚,从而致使数据不一致。
面对上述问题,咱们不得不对系统再次进行升级改造。为了解决“A银行帐户扣款了,可是写入消息队列失败”的问题,咱们须要借助一个转帐日志表,或者叫转帐流水表,该表简单的设计以下:
字段名称 | 字段描述 |
---|---|
tId | 交易流水id |
accountNo | 转出帐户卡号 |
targetBankNo | 目标银行编码 |
targetAccountNo | 目标银行卡号 |
amount | 交易金额 |
status | 交易状态(待处理、处理成功、处理失败) |
lastUpdateTime | 最后更新时间 |
这个流水表须要怎么用呢?咱们在“第1步”进行扣款时,同时往流水表写入一条操做流水,状态为“待处理”,而且这两个操做必须是原子的,也就是说必须通过本地事务保证这两个操做要么同时成功,要么同时失败!这就保证了只要转帐扣款成功,一定会记录一条状态为“待处理”的转帐流水。若是在这一步失败了,那天然就是转帐失败,没有后续操做了。若是这步操做后系统宕机了致使没有将消息成功写入消息队列(也就是“第2步”)也不要紧,由于咱们的流水数据已经持久化了!这时候咱们只须要加入一个后台线程进行补偿,按期的从转帐流水表中读取状态为“待处理”且最后更新的时间距当前时间大于某个阈值的数据,从新放入消息队列进行补偿。这样,就保证了消息即便丢失,也会有补偿机制!B银行在处理完转帐请求后会回调A银行的接口通知转帐的状态,从而更新A银行流水表中的状态字段!这样就完美解决了上一个方案中的两个不足点。系统设计图以下:
到目前为止,咱们很好的解决了消息丢失的问题,保证了只要A银行转帐操做成功,转帐的请求就必定能发送到B银行!可是该方案又引入了一个问题,经过后台线程轮询将消息放入消息队列处理,同一次转帐请求可能会出现屡次放入消息队列而屡次消费的状况,这样B银行会对同一转帐屡次处理致使数据出现不一致!那怎么保证B银行转帐接口的幂等性呢?
一样的,咱们能够在B银行系统中须要增长一个转帐日志表,或者叫转帐流水表,B银行每次接收到转帐请求,在对帐户进行操做的时候同时往转帐日志表中插入一条转帐日志记录,一样这两个操做也必须是原子的!在接收到转帐请求后,首先根据惟一转帐流水Id在日志表中查找判断该转帐是否已经处理过,若是未处理过则进行处理,不然直接回调返回! 最终的架构图以下:
因此,咱们这里最核心的就是A银行经过本地事务保证日志记录+后台线程轮询保证消息不丢失。B银行经过本地事务保证日志记录从而保证消息不重复消费!B银行在回调A银行的接口时会通知处理结果,若是转帐失败,A银行会根据处理结果进行回滚。
分布式场景,要用分布式的思惟去思考问题。要考虑任何的超时,断电,维护不一样物理存储的数据的可能存在的状态不一致的场景。面向失败编程。通常来讲,业务规则只能经过数据库惟一索引、版本号乐观并发控制、分布式锁,等这些靠谱的方式去实现。永远不要认为请求不会重复发送,消息不会重复消费,请求不会超时,数据必定一致,内存中的if判断必定有效,这种错误的想法。