SpringCloudAlibaba——Seata AT模式的实现原理

Seata AT模式的实现原理

AT模式是基于XA事务模型演化而来的,所以它的整体机制也是一个改进版的两段提交协议。

  • 第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 第二阶段:提交异步化,非常快速地完成。回滚通过第一阶段的日志进行反向补偿。

以一个创建订单事务中的库存表tbl_repo来表述整个工作过程:
在这里插入图片描述

AT模式第一阶段的实践原理:

在业务流程中执行库存扣减操作的数据库操作时,Seata会基于数据源代理对源执行的SQL进行解析,代理的配置如下:

@Bean
public DataSourceProxy dataSourceProxy(DuridDataSource druidDataSource) {
    
    
	return new DataSourceProxy(druidDataSource);
}

然后将业务数据再更新前后保存到undo_log日志表中,利用本地事务的ACID特性,把业务数据的更新和回滚日志写入同一个本地事务中进行提交,完整的执行流程如下:

在这里插入图片描述

假设AT分支事务的业务逻辑是:

在这里插入图片描述
那么第一阶段的逻辑为:

  • 通过DataSourceProxy对业务SQL进行解析,得到SQL的类型(UPDATE)、表(tbl_repo)、条件(where product_code=“GP20200202001”)等相关信息
  • 查询修改之前的数据镜像,根据解析得到的条件信息生成查询语句,定位数据:

在这里插入图片描述

得到该产品代码对应的库存数量为1000。

  • 执行业务SQL:更新这条记录的count=count-1

  • 查询修改之后的数据镜像,根据镜像的结果,通过主键定位数据
    在这里插入图片描述

    得到修改之后的镜像数据,此时count=999

  • 插入回滚日志:把前、后镜像数据及业务SQL相关的信息组成一条回滚日志记录,插入UNDO_LOG表中。可以在对应库的UNDO_LOG表中获得数据:

在这里插入图片描述
其中,rollback_info表示回滚的数据包含beforeImage和afterImage。
在这里插入图片描述

  • 提交前,向TC注册分支事务:申请tbl_repo表中主键值等于1的记录的全局锁
  • 本地事务提交:业务数据的更新和前面步骤中生成的UNDO_LOG一并提交
  • 将本地事务提交的结果上报TC

从AT模式第一阶段的实现原理来看,分支的本地事务可以在第一阶段提交完成后马上释放本地事务锁定的资源。这是AT模式和XA最大的不同点, XA事务的两阶段提交,一般锁定资源后持续到第二阶段的提交或者回滚后才释放资源。所以实际上AT模式降低了锁的范围,从而提升了分布式事务的处理效率。之所以能够实现这样的优化,是因为Seata记录了回滚日志,即便第二阶段发生异常,只需要根据UNDO. _LOG中记录的数据进行回滚即可。

AT模式第二阶段的原理分析:

TC接收到所有事务分支的事务状态汇报之后,决定对全局事务进行提交或者回滚。

事务提交:

如果决定是全局提交,说明此时所有分支事务已经完成了提交,只需要清理UNDO_ LOG日志即可。这也是和XA最大的不同点,其实在第一阶段各个分支事务的本地事务已经提交了,所以这里并不需要TC来触发所有分支事务的提交,如图所示:

在这里插入图片描述

  • 分支事务收到TC的提交请求后把请求放入一个异步任务队列中,并马上返回提交成功的结果给TC。
  • 从异步队列中执行分支,提交请求,批量删除响应UNDO_LOG日志。

在第一步中, TC并不需要同步知道分支事务的处理结果,所以分支事务才会采用异步的方式来执行。因为对于提交操作来说,分支事务只需要清除UNDO_ LOG日志即可,而即便日志清除失败,也不会对整个分布式事务产生任何影响。

事务回滚:

在整个全局事务链中,任何一个事务分支执行失败,全局事务都会进入事务回滚流程。所谓的回滚无非就是根据UNDO_ LOG中记录的数据镜像进行补偿。如果全局事务回滚成功,数据的一致性就得到了保证。全局事务回滚流程如图所示:
在这里插入图片描述

所有分支事务接收到TC的回滚请求后,分支事务参与者开启一个本地事务,执行如下操作:

  • 通过XID和branch ID查找到对饮的UNDO_LOG记录

  • 数据校验:拿UNDO_LOG中的faterImage镜像数据与当前业务表中的数据进行比较,如果不同,说明数据被当前全局事务之外的动作做了修改,那么事务将不会回滚

  • 如果afterImage中的数据和当前业务表中对应的数据相同,则根据UNDO_LOG中的beforeImage镜像和业务SQL相关信息生成回滚语句并执行:
    在这里插入图片描述

  • 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果上报给TC)

事务的隔离性保证

== 写隔离:==

写隔离是为了在多个全局事务针对同一张表的同一个字段进行更新操作时,避免全局事务在没有被提交之前被其他全局事务修改。写隔离的主要实现是,在第一阶段本地事务提交之前,确保拿到全局锁。如果拿不到全局锁,则不能提交本地事务。并且获取全局锁的尝试会有一个范围限制,如果超出范围将会放弃全局锁的获取,并且回滚事务,释放本地锁。

以一个具体的案例来分析,假设有两个全局事务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持有,所以不会发生脏写的问题。全局事务回滚时序图如图所示:

在这里插入图片描述

读隔离:

在数据库本地事务隔离级别为Read Committed或者以上时, Seata AT事务模式的默认全局事务隔离级别是Read Uncommitted。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,产生脏读。这在最终一致性事务模型中 是允许存在的,并且在大部分分布式事务场景中都可以接受脏读。

在某些特定场景中要求事务隔离级别必须为Read Committed,目前Seata是通过SelectForUpdateExecutor执行器对SELECT FOR UPDATE语句进行代理的, SELECT FOR UPDATE语句在执行时会申请全局锁。如图所示:

在这里插入图片描述

如果全局锁已经被其他分支事务持有,则释放本地锁(回滚SELECT FORUPDATE语句的本地执行)并重试。在这个过程中,查询请求会被“ BLOCKING”, 直到全局锁被拿到,也就是读取的相关数据已提交时才返回。

猜你喜欢

转载自blog.csdn.net/cold___play/article/details/108054795
今日推荐