记录一次Redisson使用synchronized和分布式锁不生效的原因

news2024/11/17 1:29:55

最近在开发的过程中,遇到了一个并发场景,用户进行方案复制的时候,当快速点击两次操作的时候,出现了复制方案重名的情况,实际上是复制方案的方案名称,是由后端根据数据库已有的方案名称和当前要复制的方案名称进行逻辑处理,保证方案名称不能重复,比如:要复制的方案名称为“我的方案”,那么复制得到的方案名称为“我的方案-副本”,在高并发场景下,就会出现重名情况。

1. 并发原因

每次在复制方案的时候,会有如下步骤:

  1. 首先校验要复制的方案是否存在。
  2. 查询所有已经存在的方案的所有名称。
  3. 根据要复制方案的名称生成一个新的方案名称,比如“某某方案-副本”。
  4. 新生成的方案是否和已存在的方案名称重名,如果重名,则添加后缀,比如“某某方案-副本(2)”。
  5. 最终做新方案的落库操作。

不知道大家有没有看到里面在高并发情况下存在的问题,当步骤五还没有落库,就已经有线程2进来,执行了查询操作,最后线程2落库生成的名称就会和线程1生成的方案名称重复。

@Transactional(rollbackFor = Exception.class)
public  void xxxCopy(Long modelId) throws GseException {
    	//业务逻辑代码
}

2. 初步解决办法

2.1 本地锁方式

我在本地做了两种尝试,首先通过本地锁(比如synchronized,Lock)相关手段进行锁定,当然这种肯定不能上生产,因为当多节点部署的时候,这种本地锁没有任何意义。

@Transactional(rollbackFor = Exception.class)
public  void xxxCopy(Long modelId) throws GseException {
    synchronized (this) {
       //业务逻辑代码
    }
}

这种写法没有生效,在我进行本地压测,开启多个线程的情况下,还是出现了重名情况,具体原因我待会会给大家分析。

2.2 分布式锁

这种才是生产上高并发经常会用到的,因为生产时多prod,采用本地锁没有任何意义,分布式锁我采用的是Redisson方案,相比较自己去写分布式锁,更稳定,更成熟。

    @Autowired
    private RedissonClient redissonClient;
    private static final String REDIS_COST_MODEL_ID_LOCK = "redis_cost_model_id_lock";
    
@Transactional(rollbackFor = Exception.class)
public  void xxxCopy(Long modelId) throws GseException {
    RLock lock = redissonClient.getLock(REDIS_COST_MODEL_ID_LOCK + modelId);
        try {
            if (lock.tryLock(20, 2, TimeUnit.SECONDS)) {
                //业务逻辑代码
            } else {
                log.error("获取分布式锁失败");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 判断当前线程是否持有锁
            if (lock.isHeldByCurrentThread()) {
                //释放当前锁
                lock.unlock();
                log.info(Thread.currentThread().getName() + "释放锁" + LocalDateTime.now());
            }
        }
}

但是,这种写法没有生效,在我本地压测的时候,还是存在重名问题。

3. 存在的问题以及原因

问题就是以上两种写法都没有生效,但是为什么呢?
在解释这个问题之前,我们首先要弄清楚两个问题:

  1. @Transactional的底层实现原理,开启事务和提交事务的时机是什么?
  2. 分布式锁,和本地锁机制释放锁的时机是什么时候?

3.1 @Transactional的底层实现原理,开启事务和提交事务的时机是什么?

它的底层实现原理主要依赖于 Spring 的面向切面编程(AOP)机制。
底层实现原理

  1. AOP 代理:当一个类或方法被 @Transactional 注解标记时,Spring 容器在初始化 Bean 时会检测到这个注解。对于使用 Spring 的代理模式(如 JDK 动态代理或 CGLIB),Spring 会为该 Bean 创建一个代理对象。这个代理对象会在调用实际方法前后插入事务管理相关的代码,即在方法执行前开启事务,在方法执行完毕后根据执行情况提交或回滚事务。

  2. 解析注解:Spring 通过扫描 Bean 定义,识别出带有 @Transactional 注解的方法或类,并配置相应的事务属性,如传播行为、隔离级别、超时时间、是否只读等。

  3. 事务拦截器:Spring 使用 AOP 机制中的拦截器(Interceptor)或Advice(通常为 TransactionInterceptor 或 AspectJ 的切面),在方法调用前后织入事务处理逻辑。在方法调用前,根据事务属性设置事务的开始;在方法正常结束时提交事务,如果方法抛出未检查异常(继承自 RuntimeException 的异常)或已检查异常(被 @Transactional 的 rollbackFor 属性指定的异常)则回滚事务。

开启事务和提交事务的时机

  1. 开启事务:事务通常在进入被 @Transactional 注解的方法之前立即开始。这意味着在执行业务逻辑之前,Spring 会确保与当前环境匹配的事务上下文已经建立。这包括选择合适的事务管理器,根据事务属性配置事务的隔离级别、传播行为等,并在数据库中实际开启事务。

  2. 提交事务:如果被注解的方法正常执行结束,没有抛出任何异常,Spring 会在离开该方法之前提交事务。提交事务意味着将所有挂起的更改永久化到数据库中,使事务中的所有操作对外可见。

  3. 回滚事务:如果在被注解的方法执行过程中抛出了异常,并且该异常未被 @Transactional 的 noRollbackFor 属性豁免,Spring 将在捕获到异常后立即回滚事务,撤销所有在事务中已完成但未提交的操作,保持数据的一致性。

3.2 分布式锁,和本地锁机制释放锁的时机是什么时候?

答案是:本地锁,如果是synchronized,看你包裹起来的范围。Lock的话 看你手动释放锁的时候。
分布式锁:看你手动释放锁的时候。

那么造成问题的原因就出来了,如下图:

在这里插入图片描述
也就是说最终提交事务和释放锁的顺序有问题,按照上面的代码写法,因为当只有方法执行完了,AOP切面才会提交事务,那么如果你将上锁的代码写到被@Transactional注解的方法里面,那么提交事务永远都会处于释放锁之后,那么在释放锁之后,提交事务之前的这段时间,就会有并发问题。

4. 正确的写法

4.1 本地锁

    public  void addLock(Long modelId)throws GseException {
        synchronized (this) {
            xxxCopy(modelId);
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
	public  void xxxCopy(Long modelId) throws GseException {
	    synchronized (this) {
	       //业务逻辑代码
	    }
	}

4.2 分布式锁

public void addLock(Long modelId) throws GseException {
        RLock lock = redissonClient.getLock(REDIS_COST_MODEL_ID_LOCK + modelId);
        try {
            if (lock.tryLock(20, 2, TimeUnit.SECONDS)) {
                xxxCopy(modelId);
            } else {
                log.error("获取分布式锁失败");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 判断当前线程是否持有锁
            if (lock.isHeldByCurrentThread()) {
                //释放当前锁
                lock.unlock();
                log.info(Thread.currentThread().getName() + "释放锁" + LocalDateTime.now());
            }
        }

    }

@Transactional(rollbackFor = Exception.class)
public  void xxxCopy(Long modelId) throws GseException {
    synchronized (this) {
       //业务逻辑代码
    }
}

这样的写法,成功避免了并发问题,被@Transactional注解的方法,在执行完毕以后,就会提交事务,然后到了调用方法里面,再去释放锁。

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

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

相关文章

【测试SQLite】测试SQLite支持的SQL语句分类

测试SQLite支持的SQL语句分类 为了全面测试SQLite支持的SQL语句,需要设计一个包含多种类型的表结构,并编写各种SQL语句来测试这些功能。目前按照以下分类进行测试: 数据定义语言(DDL)数据操作语言(DML&am…

【全网最全】2024电工杯数学建模B题53页成品论文+完整matlab代码+完整python代码+数据预处理+可视化结果等(后续会更新)

您的点赞收藏是我继续更新的最大动力! 一定要点击如下的卡片链接,那是获取资料的入口! 【全网最全】2024电工杯数学建模B题53页成品论文完整matlab、py代码19建模过程代码数据等(后续会更新)「首先来看看目前已有的资…

4. 排序算法

文章目录 1.简单排序1.1 冒泡排序1.1.1 步骤核心思想1.1.2 参考代码1.1.3 时间复杂度1.1.4 空间复杂度1.1.5 优化 1. 2. 选择排序1.2.1 核心思想1.2.2 步骤1.2.3 参考代码1.2.4 时间复杂度1.2.5 空间复杂度1.2.6 优化 1.3 插入排序1.3.1 思想1.3.2 步骤1.3.3 参考代码1.3.4 时间…

工控一体机10.1寸显示器电容触摸屏(YA1308101JK)产品规格说明书

如果您对工控一体机有任何疑问或需求,或者对如何集成工控一体机到您的业务感兴趣,可移步控芯捷科技。 一、硬件功能介绍 YA1308101JK产品介绍: YA1308101JK搭载 Android10 主流操作系统,具有系统版本更高、占用内存更低、运行效率…

Orcle查询组合字段重复的数据

oracle拼接字符串 在Oracle中,可以使用||运算符或CONCAT函数来拼接字符串。 使用||运算符: SELECT Hello, || World! AS concatenated_string FROM dual;使用CONCAT函数: SELECT CONCAT(Hello, , World!) AS concatenated_string FROM d…

Python的解析网页【XPath】

XPath 什么是XPath XPath(XML Path Language)是一种用于在XML文档中定位和选择节点的语言。它是W3C(World Wide Web Consortium)定义的一种标准查询语言,广泛用于解析和操作XML文档。 安装配置 安装lxml&#xff1…

微软Build开发者大会速览,OpenAI CEO站台剧透新模型

5月22日凌晨,微软Build 2024开发者大会在美国西雅图召开。微软CEO萨蒂亚纳德拉在会上发表主题演讲,宣布了超过50项产品更新,涵盖AI基础设施、模型产品以及生产力工具等多个领域。纳德拉强调,微软一直致力于让人工智能理解人类并帮…

AI工具助力:打造完美答辩PPT的秘诀

很多快要毕业的同学在做答辩PPT的时候总是感觉毫无思路,一窍不通。但这并不是你们的错,对于平时没接触过相关方面,第一次搞答辩PPT的人来说,这是很正常的一件事。一个好的答辩PPT可以根据以下分为以下几部分来写。 1.研究的背景和…

时间|基于SprinBoot+vue的时间管理系统(源码+数据库+文档)

时间管理系统 目录 基于SprinBootvue的时间管理系统 一、前言 二、系统设计 三、系统功能设计 1管理员功能模块 2用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取: 博主介绍:✌️大厂码农…

Unity版本使用情况统计(更新至2024年4月)

UWA发布|本期UWA发布的内容是第十四期Unity版本使用统计,统计周期为2023年11月至2024年4月,数据来源于UWA网站(www.uwa4d.com)性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势作为参考。 2023年11月 - 2024年…

ChatGPT实现法语口语练习APP

使用ChatGPT实现一个法语口语练习APP可以提供一个强大的工具,帮助学习者提高他们的口语能力。以下是一个详细的实现流程,包括需求分析、技术选型、开发、测试和部署。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎交流合…

考研数学|张宇《1000题》正确率达到多少算合格?

张宇1000题是考研数学难度较大一些的题集,题目难,计算量大,第一次做题时,如果正确率能达到60%,那就已经算是不错了。有些题目设计得挺巧妙,有时候第一次见,想不出解题思路也是正常的。 传统习题…

C51单片机开发--库函数

知不足而奋进 望远山而前行 目录 系列文章目录 文章目录 前言 目标 内容 开发过程回顾 使用库函数点灯 什么是库函数? 面向库函数和面向寄存器开发 使用delay模块延时 总结 前言 在嵌入式系统开发中,使用库函数是提高开发效率、简化编程的重要手段之一…

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《基于优先指数的配电网分布式储能序次规划》

本专栏栏目提供文章与程序复现思路,具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

我国赤泥年产量庞大 政策引导下赤泥绿色利用率将不断提升

我国赤泥年产量庞大 政策引导下赤泥绿色利用率将不断提升 赤泥是指从铝土矿中提炼氧化铝后产生的强碱性工业固体废渣,由于含大量氧化铁,表面呈现红色,而得名赤泥。   赤泥通常包含氧化铝、氧化铁、二氧化硅、氧化钙、碱金属及其他微量元素&…

【深度 Q 学习-01】 Q学习概念和python实现

文章目录 一、说明二、深度 Q 学习概念三、python实现四、结论 关键词:Deep Q-Networks 一、说明 在强化学习 (RL) 中,Q 学习是一种基础算法,它通过学习策略来最大化累积奖励,从而帮助智能体导航其环境。它…

SAP实施方法论

SAP新实施方法论 RDS Activate

分布式拒绝服务解决方式

在网络安全领域中,分布式拒绝服务(DDoS)攻击始终占据着举足轻重的地位,其影响力不容忽视。随着网络技术的日新月异和网络环境的日益复杂化,DDoS攻击不仅变得愈发频繁,而且其破坏性和影响力也呈现出惊人的增…

二区5分纯生信|单细胞+非负矩阵+AlphaFold+机器学习组合

说在前面 学文不看刊 这篇分析总体来说工作量不算大,scRNA联合bulkRNA分析,多种机器学习组合预测模型,最后用了AlphaFold2预测蛋白及AutoDock分子对接 非常适合小白学习和模仿,其次在筛选出核心基因后可以再加几个外部数据集观…

【最优化方法】实验四 约束最优化方法的MATLAB实现

实验的目的和要求:通过本次实验使学生较为熟练使用MATLAB软件,并能利用该软件进行约束最优化方法的计算。 实验内容: 1、罚函数法的MATLAB实现 2、可行方向法的MATLAB实现 学习建议: 本次实验就是要通…