当前位置: 首页 > news >正文

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

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")@Transactionalpublic 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")@Transactionalpublic 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在重试过程中,获取到全局锁,则成功执行,做到了很好的事务隔离性。

    @Overridepublic 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:11111111String 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 获取到全局锁后,进入到业务的更新操作,这里和一阶段执行本地事务完全一致,之前分析过,就不赘述了。

相关文章:

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

1 前言 在Seata 中提供了一个全局锁注解GlobalLock&#xff0c;字面意思是全局锁&#xff0c;搜索相关文档&#xff0c;发现资料很少&#xff0c;所以分析下它的应用场景和基本原理&#xff0c;首先看下源码中对该注解的说明&#xff1a; // 声明事务仅在单个本地RM中执行 //…...

Dubbo 路由及负载均衡性能优化

作者&#xff1a;vivo 互联网中间件团队- Wang Xiaochuang 本文主要介绍在vivo内部针对Dubbo路由模块及负载均衡的一些优化手段&#xff0c;主要是异步化缓存&#xff0c;可减少在RPC调用过程中路由及负载均衡的CPU消耗&#xff0c;极大提升调用效率。 一、概要 vivo内部Java…...

Python数据可视化入门指南

Matplotlib和Plotly是两个在Python中广泛使用的数据可视化库&#xff0c;它们具有丰富的API和功能&#xff0c;用于创建各种类型的图表和图形。在本篇博客中&#xff0c;我们将介绍它们的主要特点和基本用法。 Matplotlib 主要特点&#xff1a; 高度自定义&#xff1a; Matp…...

我的ChatGPT的几个使用场景

示例一&#xff0c;工作辅助、写函数代码&#xff1a; 这里展示了一个完整的代码&#xff0c;修正&#xff0c;然后最终输出的过程。GPT具备足够丰富的相关的小型代码生成能力&#xff0c;语法能力也足够好。这类应用场景&#xff0c;在我的GPT使用中&#xff0c;能占到65%以上…...

3 — NLP 中的标记化:分解文本数据的艺术

一、说明 这是一个系列文章的第三篇文章&#xff0c; 文章前半部分分别是&#xff1a; 1 — NLP 的文本预处理技术2 — NLP中的词干提取和词形还原&#xff1a;文本预处理技术 在本文中&#xff0c;我们将介绍标记化主题。在开始之前&#xff0c;我建议您阅读我之前介绍…...

C++-类与对象(上)

一、 auto关键字 1.自动识别数据类型 2.auto的初始化 3.auto简化for循环 nullptr的使用 二、类与对象 1.c中类的定义 2.c语言与c的比较 3.类的访问限定符以及封装 3.1访问限定符 3.2封装 3.3类的作用域 3.4类的声明与定义分离 &#x1f5e1;CSDN主页&#xff1a;d1ff1cult.&…...

多进程间通信学习之无名管道

无名管道&#xff1a;首先它是内核空间的实现机制&#xff1b;然后只能用于亲缘进程间通信&#xff1b;它在内核所占的大小是64KB&#xff1b;它采用半双工的通信方式&#xff1b;请勿使用lseek函数&#xff1b;读写特点&#xff1a;若读端存在写管道&#xff0c;那么有多少数据…...

flink常用的几种调优手段的优缺点

背景: 不管是基于减少反压还是基于减少端到端的延迟的目的&#xff0c;我们有时候都需要对flink进行调优&#xff0c;本文就整理下几种常见的调优手段以及他们的优缺点 flink调优手段 1.使用事件时间EventTime模式时&#xff0c;可以设置水位线发送的时间间隔&#xff0c;比…...

如何选择安全又可靠的文件数据同步软件?

数据实时同步价值体现在它能够确保数据在多个设备或系统之间实时更新和保持一致。这种技术可以应用于许多领域&#xff0c;如电子商务、社交媒体、金融服务等。在这些领域中&#xff0c;数据实时同步可以带来很多好处&#xff0c;如提高工作效率、减少数据不一致、提高用户体验…...

使用反射调用类的私有内部类的私有方法

文章目录 使用反射调用类的私有方法类实现方法实现代码 使用反射调用类的私有内部类的私有方法类实现方法实现代码 在进行单元测试时&#xff0c;我们往往需要直接访问某个类的内部类或者某个类的私有方法&#xff0c;此时正常的调用就无能为力了&#xff0c;因此我们可以使用反…...

记一次 AWD 比赛中曲折的 Linux 提权

前提背景&#xff1a; 今天一场 AWD 比赛中&#xff0c;遇到一个场景&#xff1a;PHP网站存在SQL注入和文件上传漏洞, MYSQL当前用户为ROOT&#xff0c;文件上传蚁剑连接SHELL是权限很低的用户。我需要想办法进行提权&#xff0c;才能读取到 /root 目录下的 flag。 一、sqlmap …...

[SpringCloud] Feign 与 Gateway 简介

目录 一、Feign 简介 1、RestTemplate 远程调用中存在的问题 2、定义和使用 Feign 客户端 3、Feign 自定义配置 4、Feign 性能优化 5、Feign 最佳实践 6、Feign 使用问题汇总 二、Gateway 网关简介 1、搭建网关服务 2、路由断言工厂 3、路由的过滤器配置 4、全局过…...

[Unity] 个人编码规范与命名准则参考

Unity C# 在写的过程中, 和纯 C# 是有很大出入的. 甚至说, Unity C# 就是邪教. 例如它的命名规范与 C# 是不一致的, 而且由于游戏引擎的介入, 编写时的习惯相较于 C# 来讲, 也需要有所改变. 通用编码规范 常见的一些编码规范就不需要过多提及了, 这里只做简单列举. 添加合适…...

堆栈与队列算法-以链表来实现队列

目录 堆栈与队列算法-以链表来实现队列 C代码 堆栈与队列算法-以链表来实现队列 队列除了能以数组的方式来实现外&#xff0c;也可以用链表来实现。在声明队列的类中&#xff0c;除了和队列相关的方法外&#xff0c;还必须有指向队列前端和队列末尾的指针&#xff0c;即fron…...

快速入门:使用 Spring Boot 构建 Web 应用程序

前言 本文将讨论以下主题&#xff1a; 安装 Java JDK、Gradle 或 Maven 和 Eclipse 或 IntelliJ IDEA创建一个新的 Spring Boot 项目运行 Spring Boot 应用程序编写一个简单的 Web 应用程序打包应用程序以用于生产环境 通过这些主题&#xff0c;您将能够开始使用 Spring Boo…...

01.CentOS7静默安装oracle11g

CentOS7静默安装oracle11g 一、下载Oracle11g安装包二、开始安装oracle11g三、配置Oracle监听程序四、添加数据库实例五、设置开机启动六、登录后解除锁定 一、下载Oracle11g安装包 下载链接&#xff1a;https://pan.baidu.com/s/1gcLMFGX7-8ju7OoFOFLzQA 提取码&#xff1a;6…...

SASE安全访问服务边缘

自存用&#xff1a; 参考文档&#xff1a; 什么是安全访问服务边缘 (SASE)&#xff1f; | Microsoft 安全 网安人必读 &#xff5c;一文读懂SASE - 知乎...

k8s集群升级

目录 1. 部署cri-docker &#xff08;所有集群节点&#xff09; 2. 升级master节点 3. 升级worker节点 4. 部署containerd 1. 部署cri-docker &#xff08;所有集群节点&#xff09; k8s从1.24版本开始移除了dockershim&#xff0c;所以需要安装cri-docker插件才能使用docker …...

redis原理 主从同步和哨兵集群

主从库如何实现数据一致 我们总说的 Redis 具有高可靠性&#xff0c;又是什么意思呢&#xff1f;其实&#xff0c;这里有两层含义&#xff1a;一是数据尽量少丢失&#xff0c;二是服务尽量少中断。AOF 和 RDB 保证了前者&#xff0c;而对于后者&#xff0c;Redis 的做法就是增…...

四季古诗赏析

春晓 春眠不觉晓&#xff0c;处处闻啼鸟。夜来风雨声&#xff0c;花落知多少。 夏意 别院深深夏簟清&#xff0c;石榴开遍透帘明。树阴满地日当午&#xff0c;梦觉流莺时一声。 秋词 自古逢秋悲寂寥&#xff0c;我言秋日胜春朝。晴空一鹤排云上&#xff0c;便引诗情到碧霄。 …...

【网络协议】聊聊套接字socket

网络编程我们知道是通过socket进行编程的&#xff0c;其实socket也是基于TCP和UDP协议进行编程的。但是在socket层面是感知不到下层的&#xff0c;所以在设置参数的时候&#xff0c;其实是端到端协议智商的网络层和传输层。TCP是数据流所以设置为SOCK_STREAM&#xff0c;而UDP是…...

GEO生信数据挖掘(十一)STRING数据库PPI蛋白互作网络 Cytoscape个性化绘图【SCI 指日可待】

GEO生信数据挖掘&#xff08;十&#xff09;肺结核数据-差异分析-WGCNA分析&#xff08;900行代码整理注释更新版本&#xff09; 通过 前面十篇文章的学习&#xff0c;我们应该已经可以获取到一个”心仪的基因列表“了&#xff0c;相较于原始基因数量&#xff0c;这个列表的数…...

api接口更新钉钉文档

class OperateKnowledgeBaseExcel():robot_code # agent_id #app_key #群机器人app_secret #群机器人def __init__(self,union_id, workbook_id, worksheet_id):self.union_id union_idself.workbook_id workbook_id # 获取方式&#xff1a;... &g…...

Android---如何同view进行渲染

ViewRootImpl 在 Activity、window 和 View 三者关系之间起着承上启下的作用。一方面&#xff0c;ViewRootImpl 中通过 Binder 通信机制&#xff0c;远程调用 WindowSession 将 View 添加到 Window 中&#xff1b;另一方面&#xff0c;ViewRootImpl 在添加 View 之前&#xff0…...

【LeetCode:26. 删除有序数组中的重复项 | 双指针】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…...

如何判断一个对象是不是一个空对象?

在JavaScript中&#xff0c;可以使用以下方法来判断一个对象是否为空对象&#xff1a; 1&#xff1a;使用Object.keys()方法&#xff1a;Object.keys()方法返回一个包含对象自身可枚举属性的数组。通过判断该数组的长度&#xff0c;可以确定对象是否为空对象。 function isEm…...

C/C++ “variable set but not used“的 警告问题解决方案

在编程的过程中&#xff0c;会有一些预留的变量暂时不用&#xff0c;但是编译过程编译器警告 会报错无法编译通过针对这个问题&#xff0c;采用下面的解决方案比较方便。 错误如下形式&#xff1a; 三种解决方法&#xff1a; 1.可以在变量前加上&#xff08;void&#xff09;就…...

JAVA安全入门之反射

反射 对于反射这个概念来说&#xff0c;直白的讲就是&#xff1a; 对象可以通过反射获取他的类&#xff0c;类可以通过反射拿到所有⽅法&#xff08;包括私有&#xff09;&#xff0c;拿到的⽅法可以调⽤而众所周知 JAVA 是一门静态语言&#xff0c;我们通过反射就可以达到动…...

【c++|opencv】一、基础操作---2.图像信息获取

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 图像信息获取&#xff0c;roi 1. 图像信息获取 // 获取图像信息#include <iostream> #include <opencv2/opencv.hpp>using namespace cv; …...

HarmonyOS开发:探索组件化模式开发

前言 组件化一直是移动端比较流行的开发方式&#xff0c;有着编译运行快&#xff0c;业务逻辑分明&#xff0c;任务划分清晰等优点&#xff0c;针对Android端的组件化&#xff0c;之前有比较系统的总结过相关文章&#xff0c;感兴趣的朋友&#xff0c;可以查看&#xff0c;点击…...