您现在的位置是:首页 > 正文

[图文] Seata AT 模式分布式事务源码分析

2024-04-01 07:33:57阅读 2
  1. 推荐阅读 Seata TCC 分布式事务源码分析
  2. 公众号 Young_Blog

什么是 Seata AT 模式

AT 模式是 Seata 主推的分布式事务解决方案,最早来源于阿里中间件团队发布的 TXC 服务,后来成功上云改名 GTSSeata 官方文档中有关于 AT 模式的详细介绍 —— AT Mode,它使得应用代码可以像使用本地事务一样使用分布式事务,完全屏蔽了底层细节,它和笔者之前介绍过的 Seata TCC 模式的区别有以下几点:

  1. 使用上,TCC 依赖于用户自行实现的三个方法成本较大;AT 依赖全局事务注解和代理数据源,其余代码基本不需要改动,对业务无侵入、接入成本极小
  2. TCC 的作用范围在应用层,本质上是实现针对某种业务逻辑的正向和反向方法;AT 模式的作用范围在于底层数据源,通过保存操作行记录的前后快照和生成反向 SQL 语句进行补偿操作,实现难度较大,优点是对上层应用透明
  3. TCCtry 阶段加锁,后续补偿逻辑事务间各自独立;AT 需要借助于全局锁和 GlobalLock 注解来解决不同全局事务间的写冲突问题,如果一阶段分支事务成功则二阶段一开始全局锁即被释放,否则需要夯住直到分支事务二阶段回滚完成才能释放全局锁

Seata AT 的使用方法

我们先了解一下如何在应用里使用 AT 模式,流程非常简单,Seata 也提供了 Seata-Samples 方便大家了解如何使用该项目。

第一步,增加全局事务注解

首先依赖 Seata 的客户端 SDK,然后在整个分布式事务发起方的业务方法上增加 @GlobalTransactional 注解,下面的例子来源于 Seata-Samples dubbo 案例,purchase 是事务发起方的业务方法,通过 RPC 调用了下游库存服务订单服务提供的接口:

@Override
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
public void purchase(String userId, String commodityCode, int orderCount) {
   
    LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
    // RPC 调用库存服务
    storageService.deduct(commodityCode, orderCount);
    // RPC 调用订单服务
    orderService.create(userId, commodityCode, orderCount);
    throw new RuntimeException("xxx");
}
第二步,配置代理数据源

MySQL 为例:

// 配置数据源
<bean name="accountDataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        // …… …… 省略数据源配置
</bean>

// 关键步骤,配置 Seata 的代理数据源,代理之前配置的 accountDataSource
<bean id="accountDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
    <constructor-arg ref="accountDataSource" />
</bean>

// 配置 applicationId 和 txServiceGroup,这主要是来标识应用和服务端集群的
<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
    <constructor-arg value="dubbo-demo-account-service"/>
    <constructor-arg value="my_test_tx_group"/>
</bean>

// 省略一些 dubbo 服务注册配置和 jdbcTemplate 配置
第三步,新建 undo_log 表

在事务链涉及的服务的数据库中新建 undo_log 表用来存储 UndoLog 信息,用于二阶段回滚操作,表中包含 xidbranchIdrollback_info 等关键字段信息。

Seata AT 的工作流程

工作流程总览

概括来讲,AT 模式的工作流程分为两阶段。一阶段进行业务 SQL 执行,并通过 SQL 拦截、SQL 改写等过程生成修改数据前后的快照(Image),并作为 UndoLog 和业务修改在同一个本地事务中提交

如果一阶段成功那么二阶段仅仅异步删除刚刚插入的 UndoLog;如果二阶段失败则通过 UndoLog 生成反向 SQL 语句回滚一阶段的数据修改。其中关键的 SQL 解析和拼接工作借助了 Druid Parser 中的代码,这部分本文并不涉及,感兴趣的小伙伴可以去翻看源码,并不是很复杂

图解 AT 模式一阶段流程

一阶段中分支事务的具体工作有:

  1. 根据需要执行的 SQLUPDATEINSERTDELETE)类型生成相应的 SqlRecognizer
  2. 进而生成相应的 SqlExecutor
  3. 接着便进入核心逻辑查询数据的前后快照,例如图中标红的部分,拿到修改数据行的前后快照之后,将二者整合生成 UndoLog,并尝试将其和业务修改在同一事务中提交。

整个流程的流程图如下:

值得注意的是,本地事务提交前必须先向服务端注册分支,分支注册信息中包含由表名和行主键组成的全局锁,如果分支注册过程中发现全局锁正在被其他全局事务锁定则抛出全局锁冲突异常,客户端需要循环等待,直到其他全局事务释放锁之后该本地事务才能提交。Seata 以这样的机制保证全局事务间的写隔离。

图解二阶段 Commit 流程

对服务端来说,等到一阶段完成未抛异常,全局事务的发起方会向服务端申请提交这个全局事务,服务端根据 xid 查询出该全局事务后加锁并关闭这个全局事务,目的是防止该事务后续还有分支继续注册上来,同时将其状态从 Begin 修改为 Committing

紧接着,判断该全局事务下的分支类型是否均为 AT 类型,若是则服务端会进行异步提交,因为 AT 模式下一阶段完成数据已经落地。服务端仅仅修改全局事务状态为 AsyncCommitting,然后会有一个定时线程池去存储介质(File 或者 Database)中查询出待提交的全局事务日志进行提交,如果全局事务提交成功则会释放全局锁并删除事务日志。整个流程如下图所示:

对客户端来说,先是接收到服务端发送的 branch commit 请求,然后客户端会根据 resourceId 找到相应的 ResourceManager,接着将分支提交请求封装成 Phase2Context 插入内存队列 ASYNC_COMMIT_BUFFER,客户端会有一个定时线程池去查询该队列进行 UndoLog 的异步删除。

一旦客户端提交失败或者 RPC 超时,则服务端会将该全局事务状态置位 CommitRetrying,之后会由另一个定时线程池去一直重试这些事务直至成功。整个流程如下图所示:

图解二阶段 Rollback 流程

回滚相对复杂一些,如果发起方一阶段抛异常会向服务端请求回滚该全局事务,服务端会根据 xid 查询出这个全局事务,加锁关闭事务使得后续不会再有分支注册上来,并同时更改其状态 BeginRollbacking,接着进行同步回滚以保证数据一致性。除了同步回滚这个点外,其他流程同提交时相似,如果同步回滚成功则释放全局锁并删除事务日志,如果失败则会进行异步重试。整个流程如下图所示:

客户端接收到服务端的 branch rollback 请求,先根据 resourceId 拿到对应的数据源代理,然后根据 xidbranchId 查询出 UndoLog 记录,反序列化其中的 rollback 字段拿到数据的前后快照,我们称该全局事务为 A

根据具体 SQL 类型生成对应的 UndoExecutor,校验一下数据 UndoLog 中的前后快照是否一致或者前置快照和当前数据(这里需要 SELECT 一次)是否一致,如果一致说明不需要做回滚操作,如果不一致则生成反向 SQL 进行补偿,在提交本地事务前会检测获取数据库本地锁是否成功,如果失败则说明存在其他全局事务(假设称之为 B)的一阶段正在修改相同的行,但是由于这些行的主键在服务端已经被当前正在执行二阶段回滚的全局事务 A 锁定,因此事务 B 的一阶段在本地提交前尝试获取全局锁一定是失败的,等到获取全局锁超时后全局事务 B 会释放本地锁,这样全局事务 A 就可以继续进行本地事务的提交,成功之后删除本地 UndoLog 记录。整个流程如下图所示:

本节小结

我们通过流程图分析了一下 Seata AT 模式两阶段的工作流程,这里提一句,官方文档针对 AT 模式的工作流程提供了一个非常易懂的例子 —— AT 模式工作机制

笔者强烈建议感兴趣的同学阅读过后,再看下文的源码分析

Seata AT 模式源码模块拆解

通过上面的文字和图解,相信大家已经了解了 Seata AT 模式的基本工作原理,那么本节开始我们正式进入相关源码的分析阶段。第一步,由于 Seata 模块不算少,我们先对整个 Seata 项目的模块进行拆解,挑出其中需要重点关注的模块,忽略那些次要的。

下文的源码分析均基于 Seata v0.6.1 版本

相比于之前笔者对 Seata TCC 实现的分析,AT 模式的源码就要复杂很多了,基本上大多数模块均有涉及,因此在阅读源码之前,我们先对模块的优先级进行筛选,包括下文会叙述哪些模块和忽略哪些模块

首先,seata-tccAT 的功能无关可以不用看;seata-commonseata-coreseata-configseata-discovery 这些只看名字也能知道大致的功能,后续阅读代码期间经常会看到其中的类,因此都可以暂时忽略seata-tmseata-rm 这两者都是封装的与 seata-server 进行通信的方法和步骤,这部分笔者已经在上一篇关于 TCC 的文章中叙述过了,不再赘述seata-spring 主要是注解、切面织入、方法拦截等功能的实现,关键点包括全局事务的开启,但是由于 ATTCC 在全局事务开启部分的逻辑是一致的,因此本文也不再赘述

一通排查下来,和 AT 核心功能有关的模块仅剩下 seata-rm-datasourceseata-server,仔细一想这也很合理,因为 Seata 中分支事务才是真正执行数据修改和补偿的部分,因此对于 TCC 模式来说,TwoPhaseBusinessAction 注解的实现类是分支事务,对 AT 模式来说,代理数据源正是分支事务,因此核心逻辑必然在 seata-rm-datasource 模块中,而 TC 集群是协调整个全局事务的指挥者,自然 seata-server 模块也是我们需要特别关注的,但是由于服务端逻辑和 TCC 部分高度相似,除了 v0.6.1 中新增了 DB 模式作为日志存储介质外,因此下文先选取客户端 AT 模式相关源码进行深入分析,最后简要分析下与 AT 模式相关的服务端源码

Seata AT 模式客户端部分

数据源代理部分 —— 三类 Proxy

下图来源于 Seata 官方文档:

Seata 中主要针对 java.sql 包下的 DataSourceConnectionStatementPreparedStatement 四个接口进行了再包装,包装类分别为 DataSourceProxyConnectionProxyStatementProxyPreparedStatementProxy,很好一一对印,其功能是在 SQL 语句执行前后、事务 commit 或者 rollbakc 前后进行一些与 Seata 分布式事务相关的操作,例如分支注册、状态回报、全局锁查询、快照存储、反向 SQL 生成等。

ExecuteTemplate.execute

AT 模式下,真正分支事务开始是在 StatementProxyPreparedStatementProxyexecuteexecuteQueryexecuteUpdate具体执行方法中,这些方法均实现自 StatementPreparedStatement 的标准接口,而方法体内调用了 ExecuteTemplate.execute方法拦截,下面我们来看看这个方法的实现:

public static <T, S extends Statement> T execute(SQLRecognizer sqlRecognizer,
                                                    StatementProxy<S> statementProxy,
                                                    StatementCallback<T, S> statementCallback,
                                                    Object... args) throws SQLException {
   
    
    // 如果不是处于全局事务中,即上游没有 xid 传递下来
    // 或者没有 GlobalLock 修饰,该数据操作不需要纳入 Seata 框架下进行管理
    

网站文章

  • 去除字符串中的中文 最新发布

    上述代码中,首先定义了一个字符串 str,表示需要处理的字符串。第一次替换使用了正则表达式 [\u4e00-\u9fa5]+,表示匹配任意一个中文字符,并将它们替换为一个空格。第二次替换使用了同样的正...

    2024-04-01 07:33:50
  • 计算机毕业设计django基于python的高校奖学金管理系统(源码+系统+mysql数据库+Lw文档)

    计算机毕业设计django基于python的高校奖学金管理系统(源码+系统+mysql数据库+Lw文档)

    随着互联网时代的到来,同时计算机网络技术高速发展,网络管理运用也变得越来越广泛。因此,建立一个B/S结构的高校奖学金管理系统:高校奖学金管理系统的管理工作系统化、规范化,也会提高平台形象,提高管理效率...

    2024-04-01 07:33:23
  • 使用NFS在多台Linux主机之间共享文件夹

    使用NFS在多台Linux主机之间共享文件夹

    需求 如下图,上传更新文件到在1号机器上的指定陌路上,剩下的所有的机器会自动同步更新到自己本地 NFS定义 NFS是基于UDP/IP协议的应用,其实现主要是采用远程过程调用RPC机制,RPC提供了一组...

    2024-04-01 07:33:16
  • 零拷贝技术(DMA、MMAP、sendfile)

    零拷贝技术(DMA、MMAP、sendfile)

    上述操作多次的上下文切换与拷贝会影响性能。可以使用零拷贝技术mmap+writesendfile和splice来优化。

    2024-04-01 07:33:08
  • (CL3000)MiniLED.h

    功能:用来定义控制系统常用的数据结构以及调用dll中的功能函数,直接将下载的MIniLED.h/MiniLED.cpp拷贝到开发者创建项目目录下。/* Copyright (C) Lytec Inc., 2010 *//* All rights reserved */#ifndef MiniLEDH#define MiniLEDH#include <windows.h&gt...

    2024-04-01 07:32:36
  • 老马

    一匹老马被别人卖了去拉磨. 当他被框上轭时,悲痛地说:“我从跑马场冲到了如此一个终点.”

    2024-04-01 07:32:27
  • 415 org.springframework.web.HttpMediaTypeNotSupportedException Content type 'null' not supported

    在用Postman调用微服务的时候,发现一直出现错误:{ "timestamp": 1529391330656, "status": 415, "error": "Unsupported Media Type", "exception": "org.springframework.web.HttpMediaTypeNotSupportedExceptio.

    2024-04-01 07:32:19
  • JavaWeb之HTML标签

    JavaWeb之HTML标签那些事 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录一、文件标签二、文本标签三、图片标签四、列表标签五、链接标签六、表格标签八、表单标签总结 一、文件标签

    2024-04-01 07:31:51
  • XShell远程登录华为云服务器

    XShell远程登录华为云服务器

    通过在云服务器上安装Linux系统来学习Linux的操作指令、网络编程等

    2024-04-01 07:31:45
  • 【Android studio】【Gradle】dependencies配置参数细解及异常解决

    【Android studio】【Gradle】dependencies配置参数细解及异常解决

    build.gradle配置中也许会有的疑惑。这里有答案

    2024-04-01 07:31:40