Apache Seata AT 模式事务隔离级别与全局锁设计


title: 详解 Seata AT 模式事务隔离级别与全局锁设计
author: 张乘辉
keywords: [Seata、分布式事务、AT模式、Transaction、GlobalLock]
description: Seata AT 模式的事务隔离是建立在支事务的本地隔离级别基础之上的,在数据库本地隔离级别读已提交或以上的前提下,Seata 设计了由事务协调器维护的全局写排他锁,来保证事务间的写隔离,同时,将全局事务默认定义在读未提交的隔离级别上。
date: 2022/01/12

本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。

前言

Seata AT 模式是一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

为什么要检查全局锁呢,这是由于 Seata AT 模式的事务隔离是建立在支事务的本地隔离级别基础之上的,在数据库本地隔离级别读已提交或以上的前提下,Seata 设计了由事务协调器维护的全局写排他锁,来保证事务间的写隔离,同时,将全局事务默认定义在读未提交的隔离级别上。

Seata 事务隔离级别解读

在讲 Seata 事务隔离级之前,我们先来回顾一下数据库事务的隔离级别,目前数据库事务的隔离级别一共有 4 种,由低到高分别为:

  1. Read uncommitted:读未提交
  2. Read committed:读已提交
  3. Repeatable read:可重复读
  4. Serializable:序列化

数据库一般默认的隔离级别为读已提交,比如 Oracle,也有一些数据的默认隔离级别为可重复读,比如 Mysql,一般而言,数据库的读已提交能够满足业务绝大部分场景了。

我们知道 Seata 的事务是一个全局事务,它包含了若干个分支本地事务,在全局事务执行过程中(全局事务还没执行完),某个本地事务提交了,如果 Seata 没有采取任务措施,则会导致已提交的本地事务被读取,造成脏读,如果数据在全局事务提交前已提交的本地事务被修改,则会造成脏写。

由此可以看出,传统意义的脏读是读到了未提交的数据,Seata 脏读是读到了全局事务下未提交的数据,全局事务可能包含多个本地事务,某个本地事务提交了不代表全局事务提交了。

在绝大部分应用在读已提交的隔离级别下工作是没有问题的,而实际上,这当中又有绝大多数的应用场景,实际上工作在读未提交的隔离级别下同样没有问题。

在极端场景下,应用如果需要达到全局的读已提交,Seata 也提供了全局锁机制实现全局事务读已提交。但是默认情况下,Seata 的全局事务是工作在读未提交隔离级别的,保证绝大多数场景的高效性。

全局锁实现

AT 模式下,会使用 Seata 内部数据源代理 DataSourceProxy,全局锁的实现就是隐藏在这个代理中。我们分别在执行、提交的过程都做了什么。

1、执行过程

执行过程在 StatementProxy 类,在执行过程中,如果执行 SQL 是 select for update,则会使用 SelectForUpdateExecutor 类,如果执行方法中带有 @GlobalTransactional or @GlobalLock注解,则会检查是否有全局锁,如果当前存在全局锁,则会回滚本地事务,通过 while 循环不断地重新竞争获取本地锁和全局锁。

io.seata.rm.datasource.exec.SelectForUpdateExecutor#doExecute

public T doExecute(Object... args) throws Throwable {
    Connection conn = statementProxy.getConnection();
    // ... ...
    try {
        // ... ...
        while (true) {
            try {
                // ... ...
                if (RootContext.inGlobalTransaction() || RootContext.requireGlobalLock()) {
                    // Do the same thing under either @GlobalTransactional or @GlobalLock, 
                    // that only check the global lock  here.
                    statementProxy.getConnectionProxy().checkLock(lockKeys);
                } else {
                    throw new RuntimeException("Unknown situation!");
                }
                break;
            } catch (LockConflictException lce) {
                if (sp != null) {
                    conn.rollback(sp);
                } else {
                    conn.rollback();
                }
                // trigger retry
                lockRetryController.sleep(lce);
            }
        }
    } finally {
        // ...
    }

2、提交过程

提交过程在 ConnectionProxy#doCommit方法中。

1)如果执行方法中带有@GlobalTransactional注解,则会在注册分支时候获取全局锁:

  • 请求 TC 注册分支

io.seata.rm.datasource.ConnectionProxy#register

private void register() throws TransactionException {
    if (!context.hasUndoLog() || !context.hasLockKey()) {
        return;
    }
    Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
                                                                null, context.getXid(), null, context.buildLockKeys());
    context.setBranchId(branchId);
}
  • TC 注册分支的时候,获取全局锁

io.seata.server.transaction.at.ATCore#branchSessionLock

protected void branchSessionLock(GlobalSession globalSession, BranchSession branchSession) throws TransactionException {
    if (!branchSession.lock()) {
        throw new BranchTransactionException(LockKeyConflict, String
                                             .format("Global lock acquire failed xid = %s branchId = %s", globalSession.getXid(),
                                                     branchSession.getBranchId()));
    }
}

2)如果执行方法中带有@GlobalLock注解,在提交前会查询全局锁是否存在,如果存在则抛异常:

io.seata.rm.datasource.ConnectionProxy#processLocalCommitWithGlobalLocks

private void processLocalCommitWithGlobalLocks() throws SQLException {
    checkLock(context.buildLockKeys());
    try {
        targetConnection.commit();
    } catch (Throwable ex) {
        throw new SQLException(ex);
    }
    context.reset();
}

GlobalLock 注解说明

从执行过程和提交过程可以看出,既然开启全局事务 @GlobalTransactional注解可以在事务提交前,查询全局锁是否存在,那为什么 Seata 还要设计多处一个 @GlobalLock注解呢?

因为并不是所有的数据库操作都需要开启全局事务,而开启全局事务是一个比较重的操作,需要向 TC 发起开启全局事务等 RPC 过程,而@GlobalLock注解只会在执行过程中查询全局锁是否存在,不会去开启全局事务,因此在不需要全局事务,而又需要检查全局锁避免脏读脏写时,使用@GlobalLock注解是一个更加轻量的操作。

如何防止脏写

先来看一下使用 Seata AT 模式是怎么产生脏写的:

注:分支事务执行过程省略其它过程。

业务一开启全局事务,其中包含分支事务A(修改 A)和分支事务 B(修改 B),业务二修改 A,其中业务一执行分支事务 A 先获取本地锁,业务二则等待业务一执行完分支事务 A 之后,获得本地锁修改 A 并入库,业务一在执行分支事务时发生异常了,由于分支事务 A 的数据被业务二修改,导致业务一的全局事务无法回滚。

如何防止脏写?

1、业务二执行时加 @GlobalTransactional注解:

注:分支事务执行过程省略其它过程。

业务二在执行全局事务过程中,分支事务 A 提交前注册分支事务获取全局锁时,发现业务业务一全局锁还没执行完,因此业务二提交不了,抛异常回滚,所以不会发生脏写。

2、业务二执行时加 @GlobalLock注解:

注:分支事务执行过程省略其它过程。

@GlobalTransactional注解效果类似,只不过不需要开启全局事务,只在本地事务提交前,检查全局锁是否存在。

2、业务二执行时加 @GlobalLock 注解 + select for update语句:

如果加了select for update语句,则会在 update 前检查全局锁是否存在,只有当全局锁释放之后,业务二才能开始执行 updateA 操作。

如果单单是 transactional,那么就有可能会出现脏写,根本原因是没有 Globallock 注解时,不会检查全局锁,这可能会导致另外一个全局事务回滚时,发现某个分支事务被脏写了。所以加 select for update 也有个好处,就是可以重试。

如何防止脏读

Seata AT 模式的脏读是指在全局事务未提交前,被其它业务读到已提交的分支事务的数据,本质上是Seata默认的全局事务是读未提交。

那么怎么避免脏读现象呢?

业务二查询 A 时加 @GlobalLock 注解 + select for update语句:

select for update语句会在执行 SQL 前检查全局锁是否存在,只有当全局锁完成之后,才能继续执行 SQL,这样就防止了脏读。

作者简介:

张乘辉,目前就职于蚂蚁集团,热爱分享技术,微信公众号「后端进阶」作者,技术博客(https://objcoding.com/)博主,Seata Contributor,GitHub ID:objcoding。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/583413.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

八_实验1:创建 VLAN 和划分端口

1、实验目的 通过本实验可以掌握: VLAN的概念。创建VLAN的方法。把交换机端口划分到VLAN中的方法。 2、实验​​​​​​拓扑 创建 VLAN 和划分端口的实验拓扑如下图所示。 图8-5 创建 VLAN 和划分端口的实验拓扑 3、实验步骤 (1)实验准…

力扣-有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) 注…

AI图书推荐:基于AI的商业数据分析应用

《基于AI的商业数据分析应用》(AI-Based Data Analytics: Applications for Business Management)的作者是Kiran Chaudhary、 Mansaf Alam。 本书涵盖了与营销和商业分析相关的各种主题。它探讨了组织如何通过使用数据分析及时做出更好的决策来增加利润。…

verilog分析task的接口设计,证明这种写法:assign {a,b,c,d} = links;

verilog分析task的接口设计,证明这种写法:assign {a,b,c,d} links; 1,task在状态机中的使用好处:2,RTL设计3,测试testbench4,波形分析,正确! 参考文献: 1&am…

蓝牙核心规范(core Specification)与应用规范(Profile)

加zkhengyang可申请加入蓝牙音频研究开发交流答疑群(课题组),赠送实际蓝牙耳机项目核心开发资料, 我们看到的大部分资料对于蓝牙协议分层一般是核心规范。 射频,基带,链路管理属于蓝牙硬件模块(一般由硬件实现比如FPGA) 逻辑链…

MongoDB数据库迁移的两种办法

在做系统运维时,经常需要对数据库进行迁移,今天这里分享一下MongoDB数据库数据迁移的办法。两种方法 方法1 利用NoSQLBooster for MongoDB直接复制粘贴 这种方法,适合在windows电脑上,可以直接访问原始和目标两个MongoDB库的。优…

Burp自定义插件实现请求拦截

在安全测试时,经常需要对请求进行拦截以及配置管理,以便过滤域名或路径的请求。例如:被测对象会不断收集信息(例如IP地址、设备信息)通过HTTP传给服务端。本文将介绍如何使用Burp Suite的扩展插件,通过开发…

释放Stable Diffusion 无限可能

最近在整理大语言模型的系列内容,Stable Diffusion 是我下一篇博客的主题。关注 Stable Diffusion,是因为它是目前最受欢迎和影响力最大的多模态生成模型之一。Stable Diffusion 于 2022 年 8 月发布,主要用于根据文本的描述产生详细图像&…

微服务使用SockJs+Stomp实现Websocket 前后端实例 | Vuex形式断开重连、跨域等等问题踩坑(二)

大家好,我是程序员大猩猩。 上次我们实践了,Java后端如何完成SockJSStomp的配置实现。 微服务使用SockJsStomp实现Websocket 前后端实例 | Vuex形式断开重连、跨域等等问题踩坑(一) 那么今天我们做一下web vue端的是如何来实现…

android studio拍照功能问题解决

1.点击拍照功能直接闪退 2.拍照后不能选择确认键,无法保存 上述是在android studio做项目中经常会使用到模拟器或真机的拍照功能时主要遇到的两个问题。 解决方法: 1.直接闪退问题: if(Build.VERSION.SDK_INT>Build.VERSION_CODES.N)…

谈谈进些年的BLE开发项目

加zkhengyang可申请加入蓝牙音频研究开发交流答疑群(课题组) 最早接触BLE项目是在做一款女性按摩器产品上,所谓的生活用品,用的是TI CC2640,资料齐全,上手快,配合手机app通讯开发,当然这个是单模的蓝牙芯…

学习C语言的指针

有一阵没更新了,因为最近比较繁忙,所以更新比较慢,还在慢慢学习 话不多说,开始今天的内容,聊一聊C语言指针。 很多小伙伴可能会被指针这个名字吓到,觉得很难,实际上确实有点难,但是…

cesium教程

环境搭建 vscode安装Visual Studio Code - Code Editing. Redefined nodejs安装Node.js — Run JavaScript Everywhere cesium源码下载编译 cesium官网下载源码https://cesium.com/downloads/ 解压下载的源码 VsCode打开远吗,找到index.html,右键打开 Open wit…

职场人是如何被拉开差距的?

事实上,职场人的差距从第一天就拉开了。 心理学里有一个词,叫做“首因效应,说的是人们在第一次接触时形成的印象,将会决定后续认知的基调。 入职第一天,从自我介绍开始,展示自己的特长,给大家…

unity项目《样板间展示》开发:菜单界面

unity项目《样板间展示》开发:菜单界面 前言UI菜单创建逻辑实现结语 前言 这是这个项目demo教程的最后一节,这节是菜单界面部分的创建 UI菜单创建 创建一个新的场景,在Scene文件中右键选择Create->Scene,创建新的场景 在场景…

【服务器部署篇】Linux下快速安装Jenkins

作者介绍:本人笔名姑苏老陈,从事JAVA开发工作十多年了,带过刚毕业的实习生,也带过技术团队。最近有个朋友的表弟,马上要大学毕业了,想从事JAVA开发工作,但不知道从何处入手。于是,产…

Elasticsearch实现hotel索引库自动补全、拼音搜索功能

Elasticsearch实现hotel索引库自动补全、拼音搜索功能 在这里边我们有两个字段需要用拼音分词器,一个name字段,一个all字段。 然后我们还需要去实现自动补全,而自动补全对应的字段必须使用completion类型。目前我们酒店里面所有的字段都采用的…

暴雨信息| AI“速”不可挡,倒逼算力巨变!

「 “当某一天人工智能的智慧超越人类,你会发现人工智能将会以迅雷不及掩耳之势改变世界,那个改变是不可逆的,极其迅速。” 」 暴雨信息副董事长孙辉在“IPF2024”上的这个观点,正是当今世界在AI影响下急速前行的真实写照。 记得…

高压、单通道、轨对轨输入输出功率运算放大器RS8471

RS8471是一款高压、单通道、轨对轨输入输出的功率运算放大器,它的工作电压范围在4.5V到24V,最大峰值输出电流2.5A,失调电压为3mV,增益带宽积为25MHz,并提供65V/us的高压摆率,确保输出信号快速建立”,这些特…

[Java EE] 多线程(五):单例模式与阻塞队列

1. 单例模式 单例模式是校招中最长考的设计模式之一,首先我们来谈一谈什么是设计模式: 设计模式就好像象棋中的棋谱一样,如果红方走了什么样的局势,黑方就有一定地固定地套路,来应对这样的局势,按照固定地套路来,可以保证在该局势下不会吃亏. 软件开发也是同样的道理,有很多…
最新文章