Seata入门系列【15】@GlobalLock注解使用场景及源码分析

news2024/12/22 21:47:04

1 前言

在Seata 中提供了一个全局锁注解@GlobalLock,字面意思是全局锁,搜索相关文档,发现资料很少,所以分析下它的应用场景和基本原理,首先看下源码中对该注解的说明:

// 声明事务仅在单个本地RM中执行
// 但事务需要确保要更新(或选择更新)的记录不在全局事务中
// 在上述情况下,使用此注解而不是@GlobalTransaction将有助于提高性能。
// @see io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary(Object, String, Object)用于TM、GlobalLock和TCC模式的扫描器
// @see io.seata.spring.annotation.GlobalTransactionalInterceptor#handleGlobalLock(MethodInvocation)@GlobalLock的拦截器
// @see io.seata.spring.annotation.datasource.SeataAutoDataSourceProxyAdvice#invoke(MethodInvocation) GlobalLockLogic和AT/XA模式的拦截器
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface GlobalLock {
    /**
     * 自定义全局锁重试间隔(单位:毫秒)
     * 您可以使用它覆盖“client.rm.lock.retryInterval”的全局配置,默认10
     * 注意:0或负数将不起作用(这意味着返回到全局配置)
     */
    int lockRetryInternal() default 0;

    /**
     * 自定义全局锁重试次数
     * 您可以使用它覆盖“client.rm.lock.retryTimes”的全局配置,默认30
     * 注:负数无效(这意味着返回全局配置
     */
    int lockRetryTimes() default -1;
}

源码注释大概意思:对于某条数据进行更新操作,如果全局事务正在进行,当某个本地事务需要更新该数据时,需要使用@GlobalLock确保其不会对全局事务正在操作的数据进行修改。

2 问题场景

我们参考下图,搭建一个测试案例:
在这里插入图片描述

2.1 编写代码

首先编写一个全局事务,调用订单服务下订单,扣除余额-1。

    @GlobalTransactional(rollbackFor = Throwable.class, timeoutMills = 300000)
    public void test() throws InterruptedException {
        log.info("Assign Service Begin ... xid: " + RootContext.getXID() + "\n");
        //1.创建账户 扣款
        AccountTbl accountTbl = accountTblMapper.selectById(11111111);
        AccountTbl accountTbl1 = accountTbl.setMoney(accountTbl.getMoney() - 1);
        accountTblMapper.updateById(accountTbl1);
        //2.创建订单
        orderClint.insert(accountTbl.getUserId() + "", "iphone11", 1 + "");
        // 休眠5秒
        TimeUnit.SECONDS.sleep(5);
        int i = 5 / 0;
         //模拟异常
    }

在编写一个本地@Transactional事务,直接扣除余额-1。

    @GetMapping("/GlobalLock")
    @Transactional
    public Object GlobalLock() {
        AccountTbl accountTbl = accountTblMapper.selectById(11111111);
        AccountTbl accountTbl1 = accountTbl.setMoney(accountTbl.getMoney()-1);
        accountTblMapper.updateById(accountTbl1);
        return "成功执行!!!";
    }

2.2 测试

数据库修改余额为100 元,然后测试全局事务接口,发现异常时能正常全局回滚。
在这里插入图片描述
在执行全局事务的过程中,调用GlobalLock接口,修改数据,因为全局事务接口中休眠了5秒,所以需要在访问全局接口打印全局事务日志后,快速访问GlobalLock接口。

这个时候会发现,全局事务第二阶段回滚失败,并一直在重试:
在这里插入图片描述
原因分析: 因为在全局事务执行的过程中,一阶段会直接提交本地事务,其他本地事务可直接修改该数据,所以会导致全局事务二阶段回滚时,发现数据被修改过,认为数据已经脏了,回滚失败。

2.3 解决方案

  • 手动处理:锁表,然后直接将数据修改为正常状态,但是这种比较麻烦,需要梳理脏数据的原因,也影响业务实际运行

  • 提前预防:使用@GlobalLock,在执行本地事务时,去获取该数据的全局锁,如果获取不到,说明该数据正在被全局事务执行,可以进行重试获取。

在本地修改事务上加上@GlobalLock,配置重试间隔为100ms,次数为100次,说明在10S内会不断重试获取全局锁,如果该记录在全局事务中,则会失败:

    @GlobalLock(lockRetryInternal = 100, lockRetryTimes = 100)
    @GetMapping("/GlobalLock")
    @Transactional
    public Object GlobalLock() {
        AccountTbl accountTbl = accountTblMapper.selectById(11111111);
        AccountTbl accountTbl1 = accountTbl.setMoney(accountTbl.getMoney() - 1);
        accountTblMapper.updateById(accountTbl1);
        return "成功执行!!!";
    }

2.4 注意事项

在使用@GlobalLock注解的时候,我们需要更新之前,在查询方法中添加排它锁,比如根据ID 查询时,需要如下SQL 书写:

    <select id="selectById" parameterType="integer" resultType="com.hnmqet.demo01.entity.AccountTbl">
        SELECT id,user_id,money FROM account_tbl WHERE id=#{id} FOR UPDATE
    </select>

这是因为,只有添加了 FOR UPDATE,Seata 才会进行创建重试的执行器,这样事务失败时,会释放本地锁,等待一定时间再重试。如果不添加,则会一直占有本地锁,全局事务回滚需要本地锁,则全局事务就只能等@GlobalLock事务超时失败才能拿到本地锁释放全局锁,造成@GlobalLock永远获取不到全局锁。

3 源码分析

3.1 进入拦截器

之前分析过GlobalTransactionScanner(全局事务扫描器)会扫描@GlobalLock、@GlobalTransactional注解标识的方法,并为其添加GlobalTransactionalInterceptor(全局事务拦截器)。

所以@GlobalLock标注的方法执行时,会进入到GlobalTransactionalInterceptor的invoke方法,获取@GlobalLock注解,然后进入到handleGlobalLock方法处理。
在这里插入图片描述
handleGlobalLock方法会创建一个GlobalLockExecutor匿名内部类,然后调用GlobalLockTemplate 的execute方法:

    Object handleGlobalLock(final MethodInvocation methodInvocation, final GlobalLock globalLockAnno) throws Throwable {
        return this.globalLockTemplate.execute(new GlobalLockExecutor() {
            public Object execute() throws Throwable {
                return methodInvocation.proceed();
            }
            public GlobalLockConfig getGlobalLockConfig() {
            	// 获取@GlobalLock 注解上的配置
                GlobalLockConfig config = new GlobalLockConfig();
                config.setLockRetryInternal(globalLockAnno.lockRetryInternal());
                config.setLockRetryTimes(globalLockAnno.lockRetryTimes());
                return config;
            }
        });
    }

GlobalLockTemplate模板类只有一个方法,处理逻辑也很简单,就是将注解配置塞入线程中,结束后清理:

    public Object execute(GlobalLockExecutor executor) throws Throwable {
        boolean alreadyInGlobalLock = RootContext.requireGlobalLock();
        if (!alreadyInGlobalLock) {
            RootContext.bindGlobalLockFlag();
        }
        // 将注解配置塞入ThreadLocal中
        GlobalLockConfig myConfig = executor.getGlobalLockConfig();
        GlobalLockConfig previousConfig = GlobalLockConfigHolder.setAndReturnPrevious(myConfig);
        try {
        	// 调用内部类的执行方法执行业务逻辑
            return executor.execute();
        } finally {
            //仅当这是根调用者时解除绑定。
			//否则,外部调用方将丢失全局锁标志
            if (!alreadyInGlobalLock) {
                RootContext.unbindGlobalLockFlag();
            }
            //如果前面的配置不是空的,我们需要将其设置回原来的配置
			//这样外部逻辑仍然可以使用它们的配置
            if (previousConfig != null) {
                GlobalLockConfigHolder.setAndReturnPrevious(previousConfig);
            } else {
                GlobalLockConfigHolder.remove();
            }
        }
    }

3.2 进入数据源代理

在执行业务逻辑时,因为配置了数据源代理,SQL 操作都会进入到代理数据源中,大概流程为PreparedStatementProxy.execute=>ExecuteTemplate.execute=>Executor.executor。

因为我们根据ID 查询数据时加了 FOR UPDATE(排它锁),所以执行器为SelectForUpdateExecutor,在这个执行方法中,就会进行全局锁的获取,这个时候会遇到以下几种情况:

  • 获取到全局锁,则正常执行,因为加了排它锁,其他事务都会被隔离,得等待当前事务执行完成

  • 被全局事务占有全局锁和排它锁,则会等待全局一阶段事务提交释放本地锁,GlobalLock获取到本地锁后,等待全局事务提交,释放全局锁后,再执行,

  • 如果全局失败,回滚时需要排它锁,这个时候,GlobalLock因为没有获取到全局锁抛出异常,会在异常中进行事务回滚,休眠一定时间,这个时候会让出排它锁,全局获取到排它锁后再进行全局回滚成功释放全局锁,GlobalLock在重试过程中,获取到全局锁,则成功执行,做到了很好的事务隔离性。

    @Override
    public T doExecute(Object... args) throws Throwable {
    	// 1. 获取数据库连接
        Connection conn = statementProxy.getConnection();
        // 2. 获取数据库元数据
        DatabaseMetaData dbmd = conn.getMetaData();
        T rs;
        Savepoint sp = null;
        boolean originalAutoCommit = conn.getAutoCommit();
        try {
            if (originalAutoCommit) {
                /*
                 * 为了在全局锁检查期间保持本地数据库锁
                 * 如果原始自动提交为true,则首先将自动提交值设置为false
                 */
                conn.setAutoCommit(false);
            } else if (dbmd.supportsSavepoints()) {
                /*
                 * 为了在全局锁冲突时释放本地数据库锁
                 * 如果原始自动提交为false,则创建一个保存点,然后使用此处的保存点释放db
                 * 如有必要,在全局锁定检查期间锁定
                 */
                sp = conn.setSavepoint();
            } else {
                throw new SQLException("not support savepoint. please check your db version");
            }
			// 3. 创建一个锁重试控制器
            LockRetryController lockRetryController = new LockRetryController();
            ArrayList<List<Object>> paramAppenderList = new ArrayList<>();
            // 4. SELECT id FROM account_tbl WHERE id = ? FOR UPDATE
            // 
            String selectPKSQL = buildSelectSQL(paramAppenderList);
            while (true) {
                try {
                    // #870
                    // 
                    rs = statementCallback.execute(statementProxy.getTargetStatement(), args);

                    // 尝试获取选定行的全局锁
                    // 获取主键列及值
                    TableRecords selectPKRows = buildTableRecords(getTableMeta(), selectPKSQL, paramAppenderList);
                    // 构建全局锁Key :account_tbl:11111111
                    String lockKeys = buildLockKey(selectPKRows);
                    if (StringUtils.isNullOrEmpty(lockKeys)) {
                        break;
                    }

                    if (RootContext.inGlobalTransaction() || RootContext.requireGlobalLock()) {
                        // 在@GlobalTransactional或@GlobalLock下做同样的事情
                        // 这里只检查全局锁
                        statementProxy.getConnectionProxy().checkLock(lockKeys);
                    } else {
                        throw new RuntimeException("Unknown situation!");
                    }
                    break;
                } catch (LockConflictException lce) {
                	// 如锁被占用,会抛出锁冲突异常 :LockConflictException
                	// 直接回滚,释放本地锁
                    if (sp != null) {
                        conn.rollback(sp);
                    } else {
                        conn.rollback();
                    }
                    // 触发重试,线程睡眠设置的时间,超过重试此时,则会抛出LockWaitTimeoutException 异常
                    lockRetryController.sleep(lce);
                }
            }
        } finally {
            if (sp != null) {
                try {
                    if (!JdbcConstants.ORACLE.equalsIgnoreCase(getDbType())) {
                        conn.releaseSavepoint(sp);
                    }
                } catch (SQLException e) {
                    LOGGER.error("{} release save point error.", getDbType(), e);
                }
            }
            if (originalAutoCommit) {
                conn.setAutoCommit(true);
            }
        }
        return rs;
    }

3.3 更新数据

在通过 FOR UPDATE 查询到数据后,再更新当前数据,因为查询和修改在一个@Transactional方法里,所以他们是一个事务,在查询的时候添加了排它锁,并且获取到了全局锁,才会执行到更新方法。

FOR UPDATE 获取到全局锁后,进入到业务的更新操作,这里和一阶段执行本地事务完全一致,之前分析过,就不赘述了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Map和Set【OJ练习题】

文章目录 常用的Map和Set的使用方法1.数据去重2.统计出现的次数 数组中出现次数超过一半的数字缺失的第一个正整数只出现一次的数字随机链表的复制石头和宝石 常用的Map和Set的使用方法 1.数据去重 假设有10万个数据&#xff0c;如何去重重复的数据&#xff0c;重复的数据只保…

C++基础算法⑥——信奥一本通递归算法(全排列、分解因数、菲波那契数列、Pell数列、爬楼梯、汉诺塔问题)

递归算法 1199&#xff1a;全排列1200&#xff1a;分解因数1201&#xff1a;菲波那契数列1202&#xff1a;Pell数列1204&#xff1a;爬楼梯1205&#xff1a;汉诺塔问题 1199&#xff1a;全排列 由题目可知&#xff0c;输入一个字符串&#xff0c;我们要对字符串进行所有可能的排…

基于Jsp+Servlet+MySql的汉服网站的设计与实现-源码+毕业论文

源码和文档下载地址: https://juzhendongli.store/commodity/details/16 百度云盘中存储有。

Python 编写 Flink 应用程序经验记录(Flink1.17.1)

目录 官方API文档 提交作业到集群运行 官方示例 环境 编写一个 Flink Python Table API 程序 执行一个 Flink Python Table API 程序 实例处理Kafka后入库到Mysql 下载依赖 flink-kafka jar 读取kafka数据 写入mysql数据 flink-mysql jar 官方API文档 https://nigh…

计网小题题库整理第一轮(面向期末基础)(2)

该系列第二期&#xff0c;第一期链接在这~ 计网小题题库整理第一轮&#xff08;面向期末基础&#xff09;&#xff08;1&#xff09;https://blog.csdn.net/jsl123x/article/details/134030486?spm1001.2014.3001.5501 一.选择题 1、Internet的前身是 &#xff08;C &#x…

Visual Studio远程连接Linux编译代码时,头文件在/usr/include中找不到,文件存在于/usr/include的子目录中

文章目录 1 问题的提出2 问题分析3 问题的解决 1 问题的提出 VS2022在编译数据安全传输平台时&#xff0c;远程连接到Centos上进行编译&#xff0c;但是提示找不到json头文件。 2 问题分析 在Linux系统下编译代码时&#xff0c;系统会主动到/usr/include目录主动搜索头文件。…

解释器模式——化繁为简的翻译机

● 解释器模式介绍 解释器模式&#xff08;Interpreter Pattern&#xff09;是一种用的比较少的行为型模式&#xff0c;其提供了一种解释语言的语法或表达的方式&#xff0c;该模式定义了一个表达式接口&#xff0c;通过该接口解释一个特定的上下文。在这么多的设计模式中&…

【神印王座】改编遇瓶颈,伊莱克斯无建模,皓晨加戏被绞杀,喜提五挂

【侵权联系删除】【文/郑尔巴金】 神印王座动画第78集已经更新了&#xff0c;官方实锤不会断更了&#xff0c;这可真的太爽了。龙皓晨在永恒之塔开始接受伊莱克斯的传承&#xff0c;不过剧情方面有点小瑕疵。伊莱克斯如此重要角色&#xff0c;竟然没有建模&#xff0c;龙皓晨更…

FreeRTOS 事件标志组 详解

目录 什么是事件标志组&#xff1f; 事件标志位 事件标志组 事件标志组相关 API 函数 1. 创建事件标志组 2. 设置事件标志位 3. 清除事件标志位 4. 等待事件标志位 事件标志组实操 什么是事件标志组&#xff1f; 事件标志位 表明某个事件是否发生&#xff0c;联想&am…

【JAVA学习笔记】47 - 异常,try-catch处理,throw处理

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter12/scr/com/yinhai/exception_ 〇、异常处理的引入 程序出现一个小问题如int num1 10;int num2 0&#xff1b;num1 / num2 > 10 / 0 会抛出错误&#xff0c;但这样不算致命的小问题就…

android studio启动Task配置

Android studio 高版本默认不开启Task配置&#xff0c;需要自己手动开启 1.低版本配置路径&#xff1a;&#xff08;复制他人图片&#xff09; 2.高版本路径&#xff1a;添加下图勾选配置即可 3.gradle task 3.1 初识task gradle中所有的构建工作都是由task完成的,它帮我们处…

案例精选|聚铭网络多产品联合部署为北京迎祥酒店建立信息安全屏障

北京迎祥酒店位于龙脉之上的北京后花园昌平区&#xff0c;总面积约18666平米&#xff0c;主营餐饮、住宿、汤泉、婚礼四大业务&#xff0c;酒店每一个细节都散发着国潮气息&#xff0c;充满艺术气质&#xff0c;祥瑞的照壁、精工的雕花、厚重的石刻、颇具京韵京味&#xff0c;是…

Python 自定义模块和包实现GUI(图形界面)登录界面

上一篇&#xff1a;Python 自定义模块和包设计英语生词本&#xff08;文件版&#xff09;-CSDN博客 紧接上一篇博文&#xff0c;当我们熟练掌握自定义模块和包、掌握文件的的读取与写入、掌握正则表达式内置模块"re"、掌握GUI&#xff08;图形界面&#xff09;的部分…

HarmonyOS原生分析能力,即开即用助力精细化运营

数据分析产品对开发者的价值呈现在两个层面&#xff0c;第一个是产品的层面&#xff0c;可以通过数据去洞察用户的行为&#xff0c;从而找到产品的优化点。另外一个就是运营层面&#xff0c;可以基于数据去驱动&#xff0c;来实现私域和公域的精细化运营。 在鸿蒙生态上&#…

Mac用NTFS文件夹读写NTFS硬盘 NTFS能复制多大的文件

Mac作为一款备受欢迎的计算机操作系统&#xff0c;具备了许多令人惊叹的功能和特性。然而&#xff0c;对于一些Mac用户来说&#xff0c;使用NTFS格式的硬盘可能存在一些疑问。他们可能想知道Mac是否能够读写NTFS格式的硬盘&#xff0c;以及NTFS格式的硬盘是否有文件大小的限制。…

067:mapboxGL上传CSV文件,显示图形,导出为Geojson文件

第067个 点击查看专栏目录 本示例的目的是演示如何在vue+mapbox中上传CSV文件,显示图形,导出为Geojson文件。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果使用的csv文件配置方式示例源代码(共140行)安装依赖相关API参考:专栏目标示例…

CentOS 搭建本地 yum 源方式 安装 httpd 服务

CentOS 搭建本地 yum 源方式 安装 httpd 服务 修改 yum 源 挂载光驱 mkdir -p /mnt/cdrom mount /dev/cdrom /mnt/cdromvi /etc/fstab追加以下内容&#xff1a; /dev/cdrom /mnt/cdrom iso9660 defaults 0 0手动修改CentOS-Base.repo 备份 yum 源配置文件 mv /etc/yum.re…

如何用FLStudio水果21中文版创作音乐?(官方基础教程中文版)

本文讲的是FL Studio的界面和基础工作流程。简言之&#xff0c;FL Studio能让你载入乐器和音频采样&#xff0c;通过手动输入音符数据或实时弹奏来演奏它们&#xff1b;录制外部声音&#xff08;比如用麦克风录音&#xff09;然后通过调音台来回放整个混音&#xff08;还能添加…

私有云:架构图

私有云&#xff1a;架构图 1、架构图2、服务器分配及配置3、本地物理机hosts文件配置4、相关软件包5、本地物理机电脑配置参考【内存最好20G往上】 机缘巧合之下突然想玩玩虚拟化&#xff0c;然后就查资料本地自己搭建一套私有云 使用【VMware Workstation】这个虚拟化软件来进…

Linux系统下DHCP服务安装部署和使用实例详解(蜜罐)

目录 一、概述 二、具体配置如下&#xff1a; 一、概述 DHCP &#xff1a;动态主机设置协议&#xff08;英语&#xff1a;Dynamic Host Configuration Protocol&#xff0c;DHCP&#xff09;是一个局域网的网络协议&#xff0c;使用UDP协议工作&#xff0c;主要有两个用途&…