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

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

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注解的方法,在执行完毕以后,就会提交事务,然后到了调用方法里面,再去释放锁。

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

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

相关文章

Hugging face win使用教程(二)

1. 将环境迁移到win,可以直接下载修改好的cuda环境https://download.csdn.net/download/liangjiubujiu/89368302?spm1001.2014.3001.5503 2. 修改hugging face的缓存路径 3. 重启pycharm 4. 编写测试代码,注意可能由于网络链接问题,需要关…

Parasoft C++Test软件静态分析操作指南_编码规范/标准检查

系列文章目录 Parasoft CTest软件安装指南 Parasoft CTest软件静态分析操作指南_编码规范/标准检查 Parasoft CTest软件静态分析操作指南_软件质量度量 Parasoft CTest软件静态分析_自动提取静态分析数据生成文档 Parasoft CTest软件单元测试_操作指南 Parasoft CTest软件单元…

嵌入式进阶——OLED显示器(SPI)

🎬 秋野酱:《个人主页》 🔥 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 SPI协议原理图字库芯片中文显示屏原理API的使用 SPI协议 SPI(Serial Peripheral Interface)是一种同步串行…

嵌入式要卷成下一个Java了吗?

嵌入式要卷成下一个Java了吗? 根据我十年的嵌入式开发经验,嵌入式开发的工资水平相对 Java 较低。不过,嵌入式开发岗位主要集中在第二产业,尤其是电器、电气、机械汽车、航空航天等行业的“智能制造”领域。这些行业属于重资产行…

JWT身份验证相关安全问题

前言:工作中需要基于框架开发一个贴近实际的应用,找到一款比较合适的cms框架,其中正好用到的就是jwt做身份信息验证,也记录一下学习jwt相关的安全问题过程。 JWT介绍 Json web token (JWT), 是为了在网络应用环境间传递声明而执行…

线上自由DIY预约小程序源码系统 自由DIY你的界面 带完整的的安装代码包以及搭建教程

系统概述 在当今数字化时代,移动应用成为人们生活和工作中不可或缺的一部分。预约小程序作为一种便捷的工具,为用户提供了高效的预约服务体验。而线上自由 DIY 预约小程序源码系统则为开发者和企业提供了更大的自由度和创造力,让他们能够根据…

Redis篇 String的基本命令

String基本命令 一.setnx,setex,psetex二. 增加删除命令三.append,setrange,getrange,strlen命令1.append2.setrange3.strlen4.getrange 四.String的内部编码方式 一.setnx,setex,psetex setex和psetex设置过期时间 setex设置的过期时间是秒级 psetex设置的过期时间是毫秒级 二…

11.任务状态查询API函数总结

一、任务相关 API 函数预览 二、任务相关 API 函数详解 1. 函数 uxTaskPriorityGet() 此函数用于获取指定任务的任务优先级,若使用此函数,需在 FreeRTOSConfig.h 文件中设 置配置项 INCLUDE_uxTaskPriorityGet 为 1,此函数的函数原型如下所示…

我和jetson-Nano的故事(12)——安装pytorch 以及 torchvision

在jetson nano中安装Anaconda、pytorch 以及 torchvision 1.Pytorch下载安装2.Torchvision安装 1.Pytorch下载安装 首先登录英伟达官网下载Pytorch安装包,这里以PyTorch v1.10.0为例 安装依赖库 sudo apt-get install libjpeg-dev zlib1g-dev libpython3-dev liba…

HNU-人工智能-2024期末考试回忆

前言 后知后觉,似乎是别的专业的往年试题拿来复用,我没刷到,比较吃亏。 最后一题把数据改的很恶心,让我cosplay成GPU,算3个5*5的卷积核,简直恶心。根本没时间算完。 整场考试体验还挺差的。主要是老师缝…

FreeRTOS【7】队列使用

1.开发背景 操作系统提供了多线程并行的操作,为了方便代码的维护,各个线程都分配了专用的内存并处理对应的内容。但是线程间也是需要协助操作的,例如一个主线程接收信息,会把接收的信息并发到其他线程,即主线程不阻塞&…

helm离线安装

目录 概述实践 概述 centos 7.x 离线安装 helm 3.14.4 版本 实践 离线包资源下载地址 github [roothadoop01 ~]# tar -xvf helm-v3.14.4-linux-amd64.tar.gz linux-amd64/ linux-amd64/README.md linux-amd64/LICENSE linux-amd64/helm [roothadoop01 ~]# mv ./linux-amd…

Deepin Linux 深度 V23 beige 官方源及换镜像源方法。

Deepin Linux 深度 V23 英文版本号:beige 谁起的烂名字。。。。。。 1. 打开文件管理器,在apt文件夹点右键(以管理员身份打开), 2. 输入你的登录密码,以便打开文件夹(管理员权限)。…

【会议征稿,JPCS出版】2024年航空航天与力学国际学术会议(ICAM 2024)

2024年航空航天与力学国际学术会议(ICAM 2024)将于2024年7月12-14日在中国沈阳举办。会议由东北大学机械工程与自动化学院主办,吉林大学机械与航空航天工程学院承办,大连理工大学、沈阳航空航天大学、沈阳建筑大学、沈阳工业大学、…

微火全域运营平台成优选,业内人士纷纷研究!

随着全域运营赛道的兴盛,越来越多的全域运营平台陆续上线,拓宽全域运营服务商选择空间的同时,也让全域运营平台选择成为了他们最为头疼的问题。在此背景下,各大全域运营平台背后的研发公司开始各出奇招,以获得更多全域…

MVCC 原理分析、MySQL是如何解决幻读的

文章目录 一、前言回顾1.1 事务四大特性ACID1.2 并发事务问题1.3 事务隔离级别 二、MVCC2.1 为什么使用MVCC2.2 基本概念——当前读、快照读、MVCC2.2.1 当前读2.2.2 快照读2.2.3 MVCC 2.3 隐藏字段—— TRX_ID、ROLL_PTR2.4 undo log2.4.1 介绍2.4.2 版本链 2.5 Read View读视…

【EI会议】2024年机电一体、电力与电气国际会议(ICMPE 2024)

2024年机电一体、电力与电气国际会议 2024 International Conference on Mechatronics, Power and Electrical 【1】会议简介 2024年机电一体、电力与电气国际会议即将召开,这是一场集结全球机电一体、电力与电气领域精英的学术盛宴。 本次会议旨在探讨机电一体、电…

FloodFill 算法 (下)

目录 太平洋大西洋水流问题 题解: 扫雷游戏 题解: 衣橱整理 太平洋大西洋水流问题 417. 太平洋大西洋水流问题 - 力扣(LeetCode) 题解: 如果从区域内某一个位置出发,需要向左、向上走判断是否能到达…

WordPress子比主题美化-首页动态的图片展示

WordPress子比主题首页动态的图片展示 WordPress子比主题首页添加动态的图片展示,其他程序也可以用,复制代码到相应位置即可,也可作为指定分类,重点内容等,可以适合各个场景,需要的自取。 图片展示: 教程…

香橙派AIpro开发板初体验

香橙派AIpro开发板初体验 一、引言 在当前的AI发展浪潮中,边缘计算逐渐成为了研究的热点。香橙派AIpro开发板作为一款基于昇腾AI技术的开发板,凭借其强大的算力和丰富的接口,为AI边缘计算提供了强大的支持。最近,我也是拿到了官…