这是我参与更文挑战的第14天,活动详情查看: 更文挑战
首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
一 .前言
之前分别介绍了 Seata 的启动和配置 , 这一篇来看一下 Client 端的启动过程 :
Seata 的事务发起其实可以分为2个阶段 , 第一阶段是 Client 发起 Request , 第二阶段是 Server 端对 Request 进行处理 , 以及创建 TX 对象等 , 这一篇先来看一下 Seata Client 端发起流程 , 下面来详细看一下 :
1.1 Seata AT 模式的主流程梳理
Seata 中存在三个角色 :
1 | java复制代码// TC (Transaction Coordinator) - 事务协调者 |
1.2 主要流程梳理
不说废话 , 流程图就在这 , 能看懂 , 就可以不看下文了(PS : 还不是很详细 , 后面再完整的补充) :
以下流程主要参考官方文档 , 只做个总结 , 整个流程分为2个阶段 :
针对语句 : update product set name = 'GTS' where name = 'TXC';
一阶段
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
- 查询前镜像 : 根据解析得到的条件信息,生成查询语句,定位数据。
- 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
- 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
- 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
二阶段 : 提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
1.3 GlobalLock 介绍
声明事务只在单个本地RM中执行,但是事务需要确保要更新(或选择更新)的记录不在全局事务的中间阶段
1 | java复制代码public @interface GlobalLock { |
1.4 GlobalTransactional 介绍
1 | java复制代码@Retention(RetentionPolicy.RUNTIME) |
二 . Client 事务的发起
事务的发起是有 TM 开始 , 然后由 RM 完成事务分支 , 看一下主要的流程 :
整个流程这一次只说前面2步 :
- Interceptor 对操作进行拦截
- TransactionalTemplate 处理事务整体流程
看一下类流程 :
1 | java复制代码// TM 流程一 :注解处理阶段 , 此阶段方法上标注注解 , 将创建一个新的事务 |
2.1 注解的入口
1 | java复制代码// 注解的扫描方式 : |
PS:C01_001 AbstractAutoProxyCreator 作用
简单点说 , 就是为这个类做了代理 . 使用AOP代理包装每个合格bean的BeanPostProcessor实现,在调用bean本身之前将委托给指定的拦截器。
GlobalTransactionScanner 详情
详见 Client 配置 >>>> Client端配置流程
2.2 注解的拦截
通常方法上标注注解来实现某种功能 , 其最终原理都是代理 , 不论是Java 原生得 Proxy 类创建的 , 还是 Aop 实现的 , 其核心是一致的.
Step 1 : 代理的方式
此处使用的代理是 CglibAopProxy
Step 2 : 调用的流程
第一个调用的对象为 GlobalTransactionalInterceptor , 由其 invoke 方法完成了相关的代理操作
PS : 我原本以为框架会优先扫描所有的标注了 @GlobalTransactional 的方法 , 并且通过相关的Manager 进行管理 , 但是单独看这一处的代码 , 采用的是代理后直接获取的方式
GlobalTransactionalInterceptor # invoke 主流程
1 | java复制代码// 先来纵观一下 invoke 方法主流程 |
GlobalTransactionalInterceptor 其他主要方法
1 | java复制代码 |
[PRO22001] : methodInvocation 详情
[PRO22002] : TransactionInfo 详情
总结 :
从此处可以看到 , 在GlobalTransactionalInterceptor 拦截器中 , 首先调用了 template 方法 , 同时把 executor 需要执行的具体方法传入 , 而 method 通过 invoke 代理调用
另外 , 处理异常是在 拦截器 中执行 ,而不是 Template 的模板类型
三 . Client 事务的逻辑处理
Client 的事务处理为 TransactionalTemplate 模板方法来完成 , 来看一下 TransactionalTemplate 的功能
上一步当拦截器拦截完成后 , 就会来到 template 处理环节 , 该环节最核心的类为 : TransactionalTemplate , 这一步是模板方法设计模式 , 这一步中会调用 TransactionalExecutor 执行最终的逻辑
1 | java复制代码C51- TransactionalTemplate |
可以看到 , 这个中已经把整个事务处理的主流程完成了
接上一小结 , 当执行 business.execute()
后出现异常 , 会在 execute 中处理 , 而 Template 中 , 主要是对事务的管理和回退等操作 , 这也是使用模板方法设计模式的核心 , 只涉及统一的逻辑
总结
这一篇我们看到了 Seata 逻辑的发起流程 ,可以看到 ,通过以下几个步骤发起了事务的处理 :
- beginTransaction(txInfo, tx)
- business.execute()
- completeTransactionAfterThrowing(txInfo, tx, ex)
- commitTransaction(tx)
后面一篇我们来看一下后面这几个步骤做了什么>>> 👉
附录 . 补充点
读隔离和写隔离可以参考官方文档 👉 Seata 隔离策略
4.1 读隔离详情
Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) , 其通过 SELECT FOR UPDATE 实现
流程详情
- SELECT FOR UPDATE 语句的执行会申请 全局锁
- 如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试 (意味着会一直等待正在的全局提交完成)
这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回
PS : 这里产生了疑问 , 如果插入比较多的情况 , 会导致全局锁一直被占用或者频繁占用 , 所以这里还是要做读写分离
4.2 写隔离详情
写隔离是保证事务统一的关键 , 其中涉及到全局锁和本地锁的特性 , 为了保证准确 , 使用如下模式 :
1,2 分别代表2个事务 :
一阶段提交流程
- 开启本地事务 , 拿到本地锁 ,再拿到 全局锁 , 提交本地事务 , 释放本地锁 (PS :此时始终持有全局锁)
- 其他事务提交 , 开启本地事务 , 拿到本地锁 ,等待全局锁 (PS :此时上一个事务持有全局锁)
二阶段成功提交流程
- 全局处理完成 , 释放全局锁
- 其他事务 拿到全局锁 , 按照原流程处理
二阶段回退流程 (如果出现异常导致 1 事务回退)
1 . 发生异常 , 尝试回退 , 等待本地锁 (PS : 因为本地锁被其他事务持有)
2 . 始终无法拿到全局锁 , 本地事务回退 , 释放本地锁
1 . 拿到 本地锁 ,完成回退
本文转载自: 掘金