一文读懂分布式事务一致性

转载自:https://blog.csdn.net/w05980598/article/details/79305239git

1、概述程序员

在支付、交易、订单等强一致性系统中,咱们须要使用分布式事务来保证各个数据库或各个系统之间的数据一致性。github

举个简单的例子来描述一下这里数据一致性的含义。 数据库

程序员小张向女朋友小丽转帐100人民币,转帐过程是:先扣除小张100元,再为小丽的帐户添加100元。编程

若是在转账过程当中,扣款操做和打款操做要么同时执行,要么同时都不执行,咱们就认为转账过程保证了数据一致性。架构

上面的例子中,若是咱们不使用分布式来保证转帐过程当中数据的一致性,就有可能出现小张帐户上的钱被扣除,而小丽帐户上的钱却没被添加的状况,其结果你们能够自行脑补。并发

事务是数据库特有的概念,分布式事务最初起源于处理多个数据库之间的数据一致性问题,但随着IT技术的高速发展,大型系统中逐渐使用SOA服务化接口替换直接对数据库操做,因此如何保证各个SOA服务之间的数据一致性也被划分到分布式事务的范畴。 异步

本文将从一个最为简单的交易系统出发,由浅入深地讲述分布式事务架构的演进过程,但愿对你们理解分布式事物架构有所帮助。分布式

 

2、单数据库事务高并发

先来看看咱们须要实现的交易系统:游戏中的玩家使用金币购买道具,交易系统须要负责扣除玩家金币并为玩家添加道具。

咱们把交易系统的一次交易流程概括为两步:

  1. 扣除玩家金币

  2. 为玩家添加道具

需求并不复杂,咱们为金币系统在数据库中添加金币表,为道具系统在数据库中添加道具表,扣除金币与添加道具的操做只需执行相应的SQL便可。

这里咱们假设金币表与道具表都在同一个数据库中,因而能够简单地使用单数据库事务来保证数据的一致性。

下面是使用单数据库事务进行一次正常交易的时序图:

 上图演示了一次正常交易的流程,通常状况下正常的交易流程不会产生数据不一致问题。

下面讨论当出现异常时,如何使用单数据库事务保证数据一致性: 

  1. 在步骤[2]执行SQL扣除金币时出现异常,回滚事务便可保证数据一致

  2. 在步骤[4]执行SQL添加道具时出现异常,回滚事务便可保证数据一致

  3. 在步骤[6]提交事务时出现异常,回滚事务便可保证数据一致

经过上面三种异常的处理方式,咱们不难看出,其实使用单数据库事务保证数据一致性特别简单,只需没有异常提交事务而出现异常回滚事务便可。

3、基于后置提交的多数据库事务

 

随着玩家数量激增,金币表与道具表的总行数与访问量都急剧扩大,单台数据库不足以支撑起这两张表的读写请求,这时将金币表与道具表放在不一样的数据库中是个不错的选择。 

这里咱们假设金币表被放入了金币数据库中,而道具表被放入了道具数据库中,一般咱们将这种按不一样业务拆分数据库的方式称之为数据库垂直拆分。 

数据库垂直拆分能大大缓解数据库的压力问题,但多个数据库的存在乎味着咱们不能经过简单的单数据库事务来保证数据的一致性,如何保证多数据库之间数据的一致性,也就是分布式事务须要解决的问题。

回到咱们的交易系统,先不考虑多数据库之间的数据一致性问题,简单的交易流程为:

 正常状况下,上面的流程不会产生数据一致性问题,但若是在步骤[7]执行SQL添加道具时出现异常,因为扣除金币的事务已经在步骤[5]提交没法回滚,就会出现扣除玩家金币后没有为玩家添加道具的数据不一致状况。

上面问题产生的缘由实际上是过早地向金币数据库提交事务,因此咱们能够采起后置提交事务策略来解决此问题,即先在金币数据库与道具数据库上执行SQL,最后再提交金币数据库与道具数据库上的事务,这样当执行SQL出现异常时,咱们就能经过同时回滚两个数据库上事务的方式,来保证数据一致性。

下面是使用后置提交事务进行一次正常交易的时序图:

 结合上图,咱们讨论当出现异常时,后置提交事务如何避免数据不一致问题:

  1. 在步骤[3]执行SQL扣除金币时出现异常,回滚金币数据库上的事务便可保证数据一致

  2. 在步骤[5]执行SQL添加道具时出现异常,同时回滚金币数据库与道具数据库上的事务便可保证数据一致

  3. 在步骤[7]提交扣除金币事务时出现异常,同时回滚金币数据库与道具数据库上的事务便可保证数据一致

  4. 在步骤[9]提交添加道具事务时出现异常,因为扣除金币事务已提交没法回滚,会出现扣除玩家金币后没有为玩家添加道具的数据不一致状况

经过上面四种异常的处理方式,咱们能够看出,使用后置提交事务的策略,虽然能避免SQL执行异常致使的数据不一致,但在最后提交事务遇到异常时却无能为力,因此咱们须要引入新的事务提交方式。

4、基于两段提交的多数据事务

因为传统方式的事务提交没法完美保证多个数据库之间数据的一致,因而计算机科学家们引入了两段式事务提交(这是目前多数据库分布式事务的通用解决方案)。

所谓两段式事务提交指的是在执行提交commit操做前,添加预提交prepare操做,其中预提交操做执行了传统意义上提交操做的大部分工做,咱们能够简单地认为只要预提交prepare操做成功,后续的提交commit操做必定会成功。

咱们将购买道具的交易流程改成两段提交,时序图以下:

 上图中,咱们在真正提交事务以前采用了预提交事务,预提交是一个很重的操做,一旦抛出异常,咱们回滚事务便可,但预提交一旦成功,后续的提交操做则很是轻量,几乎能够认为不会出错,这样咱们就使用两段提交保证了多个数据库之间数据的一致性。

其实上面的两段式事务也就是著名的XA事务,XA是由X/Open组织提出的分布式事务的规范,也是目前使用最为普遍的多数据库分布式事务规范。

通常状况下,咱们在使用XA规范编写多数据库分布式事务代码时,不用本身去实现两段提交代码,而是使用atomikos等开源的分布式事务工具。

下面是一个使用atomikos实现简单分布式事务(XA事务)的源码:

github.com/liangyanghe/xa-transaction-demo

5、TCC事务

 以前咱们的交易系统在进行购买道具时,都是直接操做金币表与道具表,下面咱们对交易系统的架构进行升级:

 将与金币相关的操做独立成一套金币服务,将与道具相关的操做独立成一套道具服务,交易系统在扣除金币与添加道具时,再也不直接操做数据库表,而是调用相应服务的SOA接口。

 基于SOA接口的最简交易时序图以下:

 上图中,咱们的交易系统再也不直接操做数据库表,而是经过调用SOA接口的方式扣除金币与添加道具。

咱们考虑在步骤[3]调用SOA接口添加道具时出现异常,因为以前已经调用SOA接口扣除金币成功,因而就会出现扣除玩家金币后,没有为玩家添加道具的不一致状况。

为保证各个SOA服务之间的数据一致性,咱们须要设计基于SOA接口的分布式事务。 

目前比较流行的SOA分布式事务解决方案是TCC事务,TCC事务的全称为:Try-Confirm/Cancel,翻译成中文即:尝试、肯定、取消。 

简单来讲,TCC事务是一种编程模式,若是SOA接口的提供者与调用者都听从TCC编程模式,那么就能最大限度的保证数据一致性。 

下面咱们以扣除金币这一操做,来讲明一下TCC编程模式。 

非TCC模式的扣除金币操做,接口提供者只须要提供一个SOA接口便可,接口的做用就是扣除金币。 

而TCC模式的扣除金币操做,接口提供者针对扣除金币这一操做须要提供三个SOA接口:

  1. 扣除金币Try接口,尝试扣除金币,这里只是锁定玩家帐户中须要被扣除的金币,并无真正扣除金币,相似于信用卡的预受权;假设玩家帐户中100金币,调用该接口锁定60金币后,锁定的金币不能再被使用,玩家帐户中还有40金币可用

  2. 扣除金币Confirm接口,肯定扣除金币,这里将真正扣除玩家帐户中被锁定的金币,相似于信用卡的肯定预受权完成刷卡

  3. 扣除金币Cancel接口,取消扣除金币,被锁定的金币将返还到玩家的帐户中,相似于信用卡的撤销预受权取消刷卡

SOA接口调用者如何使用这三个接口呢?

调用者先执行扣除金币Try接口,再去执行其余任务(好比添加道具),当其余任务执行成功,调用者执行扣除金币Confirm接口确认扣除金币,而当其余任务执行异常,调用者则执行扣除金币Cancel接口取消扣除金币。

这里咱们假设添加道具的SOA接口也知足TCC模式,下图是使用TCC事务进行道具购买的时序图:

 对照上图,咱们分析一下TCC事务如何在各类异常状况下,保证数据的一致性:

  1. 在步骤[1]调用扣除金币Try接口时出现异常,调用扣除金币Cancel接口便可保证数据一致

  2. 在步骤[3]调用添加道具Try接口时出现异常,调用扣除金币Cancel接口与添加道具Cancel接口便可保证数据一致

  3. 在步骤[5]调用扣除金币Confirm接口时出现异常,调用扣除金币Cancel接口与添加道具Cancel接口便可保证数据一致

  4. 在步骤[7]调用添加道具Confirm接口时出现异常,因为扣除金币操做已经肯定不能再取消,因此这里会引起数据不一致

 经过上面四种异常,咱们能够看出,即便咱们使用了TCC事务,也没法完美的保证各个SOA服务之间的数据一致性。

但TCC事务为咱们屏蔽了大多数异常致使的数据不一致,同时通常状况下,进行Confirm或Cancel操做时产生异常的几率极小极小,因此对于一些强一致性系统,咱们仍是会使用TCC事务来保证多个SOA服务之间的数据一致性。

6、最终一致性 

有了TCC事务,咱们可以保证多个SOA服务之间的数据一致性,但细心的朋友可能已经发现,TCC事务存在不小的性能问题。

为了描述性能问题的产生,咱们将交易系统的需求略做修改:游戏中的玩家使用金币购买道具A,系统将自动赠送给玩家道具B,道具C与道具D。

这里咱们假设咱们到道具服务不支持批量添加道具,而只有基于TCC模式的添加单个道具的接口。

为保证数据一致性,交易系统须要先调用扣除金币Try接口,而后再依次调用添加道具A、B、C、D的Try接口,最后再依次调用对应的Confirm接口。

因为TCC事务是先Try再Confirm的模式,接口调用量会翻倍,这在接口调用量小时性能影响并不明显,但上面的需求中咱们执行扣除金币,添加道具A、B、C、D共有5个接口调用,翻倍后变为10个,系统性能会大大下降。

那么是否有既能保证数据一致性,又能保证性能的分布式事务方案?

在回答这个问题以前,咱们先将事务一致性划分为两类:

  1. 强一致性事务,请求结束后,数据就已经一致

  2. 最终一致性事务,请求结束后,数据没有一致,但一段时间后数据能保持一致

 

其实咱们使用的基于后置提交的多数据库事务与TCC事务都属于强一致性事务,使用强一致性事务能保证事务的实时性,但却很难在高并发环境中保证性能。

再来看最终一致性事务,最终一致性事务这几个字看起来很牛逼,但说白了就是异步数据补偿,即在核心流程咱们只保证核心数据的实时数据一致性,对于非核心数据,咱们经过异步程序来保证数据一致性。

因为最终一致性事务引入了异步数据补偿机制,主流程的执行流程被简化,性能天然获得提升。

目前主流触发异步数据补偿的方式有两种:

  1. 使用消息队列实时触发数据补偿,核心流程在保证核心数据的一致性后,使用消息队列的方式通知异步程序进行数据补偿,这种方式能近乎实时的使数据达到最终一致性,但若是消息队列或异步程序出现异常,数据一致性也将不能保证

  2. 使用定时任务周期性触发数据补偿,核心流程在保证核心数据的一致性后直接返回,由定时任务周期性触发数据补偿程序,这种方式虽不能像消息队列那样能近乎实时的使数据达到最终一致性,但数据补偿程序出现异常时,咱们能比较容易在下个周期对数据进行修复,能最大限度的保证数据的一致性

 

上面两种异步数据补偿的方式各有利弊,消息队列方式实时性强,但在异常状况下一致性弱,而定时任务方式实时性弱,但在异常状况下一致性强。

其实最优的策略是同时使用消息队列与定时任务触发数据补偿。

正常状况下,咱们使用消息队列近乎实时的异步触发数据补偿,而针对那些极少发生的异常,咱们使用定时任务周期性的修补数据。

这样在正常状况下,咱们能近乎实时的使数据达到最终一致性,而对于一些异常数据则按照定时任务的执行周期,周期性的达到最终一致性。

回到上面的新版交易系统:游戏中的玩家使用金币购买道具A,系统将自动赠送给玩家道具B,道具C与道具D。

下图是使用消息队列实时触发数据补偿实现最终一致性的时序图(如看不清楚能够点击图片放大):

 上图中,咱们使用TCC事务保证了扣除金币与添加道具A数据一致,而后发送赠送消息并结束请求,赠送系统收到消息后负责添加道具B、C、D,最终保证数据一致。

 这里若是消息队列或赠送服务出现异常咱们的最终一致性将难以保证,因此咱们能够再引入一个定时任务,周期性的触发异常数据补偿。

这样咱们就实现了一个既能保证最终数据一致,又能保证性能的道具买赠系统。

相关文章
相关标签/搜索