目录
说到事务,我们第一个想到的应该是数据库的事务,一个逻辑单元中的多个业务操作,要么全部成功,要么全部失败,必须满足ACID四个特性,它们分别是:
这里的ACID特性,是针对单体架构下单库单表的事务需要的。随着业务的发展,单体架构慢慢演变为微服务架构,同时跟着变化的是业务的拆分和数据库的拆分。一个比较典型的场景是,电商系统的订单和库存业务,分别拆分成了独立的服务,用户下单的时候,订单服务完成订单的创建,库存服务完成库存的扣减,如下图所示:
在微服务架构这样的场景下,原本单个库的事务操作变成了多个库的事务操作,每个库的执行情况相互之间是不知道的,可能订单创建成功,但是库存扣减失败。这就是我们接下来要聊的分布式事务问题。
那如果要给分布式事务下一个定义,会是怎么样的呢?下面是一个参考的定义:
事务的参与者、支持事务的服务器、资源服务器及事务管理器分别位于分布式系统的不同节点上。?
分布式事务产生的核心原因是存储资源的分布性,比如多个数据库,或者MySQL和Redis两种不同存储设备的数据一致性等。这里会有一个特例,一个数据库,不同的服务跨了JVM,也会产生分布式事务问题。我们在实际应用中,能避免分布式事务问题就应该去避免,以免为系统带来额外的复杂度。
X/Open DTP(X/Open Distributed Transaction Processing Reference Model)是X/Open 这个组织定义的一套分布式事务的标准。这个标准提出了使用两阶段提交来保证分布式事务的完整性。如下图所示,X/Open DTP中包含以下三种角色:
事务执行流程图如下:
?需要注意的是,TM和多个RM之间的事务控制,是基于XA协议(XA Specification)来完成的。XA协议是X/Open提出的分布式事务处理规范,也是分布式事务处理的工业标准,它定义了xa_和ax_系列的函数原型及功能描述、约束等。目前 Oracle、MySQL、DB2都实现了XA接口,所以它们都可以作为RM。
接下来我们再来继续看看大家常说的两阶段提交协议和三阶段提交协议。
从上面我们可以看出,TM实现了对多个RM事务的管理,这里会涉及两个阶段的提交,第一个阶段是事务的准备阶段,第二个阶段是事务的提交或者回滚阶段,这两个阶段都是由TM事务管理器发起的。具体执行流程如下:
完整的执行流程图如下:
存在的问题:
三阶段提交协议在二阶段的基础上,增加了超时机制来解决同步阻塞的问题。具体执行流程如下:
完整的执行流程图如下:
?三阶段提交协议,相对于两阶段提交协议,它的优势在于:
不管是两阶段提交协议,还是三阶段提交协议,本质上都是保证数据一致性的强一致性解决方案,是XA协议解决分布式数据一致性问题的基本原理。在实际应用当中,除非对数据一致性有严格要求必须强一致性,比如银行、金融等系统,一般情况下我们不会选择强一致性解决方案。
接下来我们要说一下分布式事务的两个理论模型:CAP定理和BASE理论。
CAP定理
在分布式系统中,不可能同时满足一致性(C-Consistency,多个副本的数据要保持一致)、可用性(A-Avalibility,服务一直处于可用)、分区容错性(P-Partition Tolerance,任何网络分区故障,系统任然可对外提供服务)这三个需求,最多同时满足两个。
由于网络通信并不是绝对可靠的,即使出现网络故障,系统任要能对外提供服务。所以分区容错性是一定存在的,最后我们就只能选择AP或者CP模式来保证分布式系统中的数据一致性问题。为何不能是CA模式呢?如果选择了CA,那么网络通信是绝对可靠的。如果网络出现故障,为了保证C,那么就要拒绝客户端的请求,这就没办法保证A。
AP:放弃强一致性,保证最终一致性,这是目前多数场景下解决分布式数据一致性的方案。
CP:放弃高可用,实现强一致性,前面的两阶段或三阶段提交协议就是这种模式,用户的体验可能没那么好,需要等待更长的时间。
BASE理论
由于CAP定理中无法同时满足一致性和可用性,就衍生出了BASE理论,其核心思想就是通过牺牲一致性来保证可用性,只要数据能最终达到一致就行,它的三个特性如下:
在实际应用当中,大多数情况下我们会选择BASE理论来实现数据的最终一致性,对用户来说,系统的可用性大部分时候更重要。
TCC补偿型方案把一个完整的业务拆分成了下面三个步骤:
TCC执行流程图如下:
?该方案比较明显的缺点就是对业务侵入性比较大,每一个业务操作都要准备一个Confirm方法和Cancel方法。
该方案是利用消息中间件的可靠机制来实现数据的一致性保证。其执行流程图如下:
该方法和基于可靠性消息的最终一致性方案类似,最典型的应用场景就是支付场景的回调。
?这里的最大努力通知是指,在客户端如果没有一个确定的状态时,也就是因为一些异常原因,没有成功更新业务系统的订单状态,支付宝会不断重试,直到客户端收到消息确认或者达到最大重试次数。
Seata是一款开源的分布式事务解决方案,它提供了AT、TCC、Saga和XA四种事务模式,为开发者提供了一站式的分布式事务解决方案。在实际开发过程中,只需要一个注解即可解决分布式事务问题。
AT模式是Seata主推的分布式事务解决方案,基于XA演变而来,它同样有TM(事务管理器)、RM(资源管理器)和TC(事务控制器)三种角色,其中TC作为Seata的服务端独立部署。AT事务模式如下图:
其具体执行流程如下:
Saga模式又叫长事务解决方案,它的核心思想是把一个业务流程中的长事务拆分为多个本地短事务。
从上图我们看到,Saga模式是由一系列短事务Ti组成,每一个Ti都对应了一个补偿事务,每一个Ti都会真实影响数据库数据。?该模式我们在这里就只做简单了解,本文我们主要还是深入学习主推的AT模式。
AT模式的整体机制本质上是一个改进版的两阶段提交协议,它的两个阶段分别是:
下面我们就一起来分析AT模式每一个阶段的实现原理,下面是我们分析要用的测试表结构(表名为tbl_repo)及数据。
这里我们拿库存扣减作为例子,在执行库存扣减的数据库操作时,seata会解析执行的sql,然后将修改前后的业务数据保存到undo_log日志表中,业务数据的修改和回滚日志的记录在本地同一个事务中完成。完整的执行流程如下图:
?假设这里我们要进行库存扣减的业务操作sql如下:
update tbl_repo set count = count - 1 where product_code = 'GP20200202001'
?那么第一阶段做了如下事情:
select id, product_code, name, count from tbl_repo where product_code = 'GP20200202001'
?这里的rollback_info信息会是下面这个样子:
事务控制器TC收到所有分支事务上报的结果之后,决定全局事务是提交还是回滚。
事务提交
如果是全局事务提交,那么只需要清理对应的UNDO_LOG日志即可,因为这个时候所有分支事务已经完成提交。
事务回滚
如果是全局事务回滚,那么就根据记录的UNDO_LOG日志,进行相应事务的补偿。
从前面的分析,我们可以知道,UNDO_LOG日志中记录了业务数据修改之后的数据镜像,通过全局事务ID和分支事务ID 找到UNDO_LOG记录,拿到记录中的afterImage,然后和当前业务表中的数据进行比较,如果相同,说明可以回滚;如果不相同,说明其他事务对业务表中的数据进行了修改,不能进行回滚。
我们知道,在多个并发事务同时操作同一个表的同一条数据的时候,它们之间不能相互干扰,这就是我们接下来要说的事务的隔离性。
AT模式下,事务的隔离性是通过全局锁来实现的。
写隔离
在第一个阶段本地事务提交之前,确保拿到全局锁,如果拿不到全局锁,则不能提交本地事务。并且获取全局锁的尝试会有一个范围限制,如果超出范围将会放弃全局锁的获取,并且回滚该事务,释放本地锁。
案例:假设有两个全局事务TX1和TX2,分别对 tbl_repo表的count字段进行更新操作,count的初始值为100。
TX1先执行,开启本地事务,拿到本地锁(数据库级别锁),更新count=count-1=99。在本地事务提交之前,需要拿到该记录的全局锁,然后提交本地事务并释放本地锁。
TX2接着执行,同样先开启本地事务,拿到本地锁,更新count=count-1=98。本地事务提交之前,也尝试获取该记录的全局锁(全局锁由TC控制,由于该全局锁已经被TX1获取了,所以TX2需要等待以重新获取全局锁。如果全局事务执行整体提交,那么提交时图如下图所示:
如果TX1在第二阶段执行全局回滚,那么TX1需要重新获得该数据的本地锁,然后根据UNDO_LOG进行事务回滚。此时,如果TX2仍然在等待该记录的全局锁,同时持有本地锁,那 TX1分支事务的回滚会失败。TX1分支事务的回滚过程会一直重试,直到TX2的全局锁获取超时,放弃全局锁并回滚本地事务、释放本地锁,之后TX1的分支事务才会回滚成功。而在整个过程中,全局锁在TX1结束之前一直被TX1持有,所以不会发生脏写的问题。全局事务回滚时序图如下图所示:
读隔离
数据库事务的4中隔离级别:Read Uncommitted(读未提交);Read Committed(读已提交);Repeatable Read(可重复读);Serializable(可串行化)。
在数据库本地事务隔离级别为Read Committed 或者以上时,Seata AT事务模式的默认全局事务隔离级别是Read Uncommitted。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,产生脏读。这在最终一致性事务模型中是允许存在的,并且在大部分分布式事务场景中都可接受脏读。
在某些特定场景中要求事务隔离级别必须为Read Comitted,目前Seata是通过SelectForUpdateExecutor执行器对SELECT FOR UPDATE语句进行代理的,SELECT FOR UPDAT语句在执行时会申请全局锁。
如下图所示,如果全局锁已经被其他分支事务持有,则释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。在这个过程中,查询请求会被“BLOCKING”,直到全局锁被拿到,也就是读取的相关数据己提交时才返回。