专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
北京药监  ·  北京药品医疗器械创新服务站咨询安排(2025 ... ·  14 小时前  
北京药监  ·  北京药品医疗器械创新服务站咨询安排(2025 ... ·  14 小时前  
南昌网警  ·  真相来了:所谓“隔空盗刷”不具备技术可行性 ·  17 小时前  
南昌网警  ·  真相来了:所谓“隔空盗刷”不具备技术可行性 ·  17 小时前  
BRTV建外14号  ·  本周起,北京地铁3号线、12号线、19号线有 ... ·  20 小时前  
BRTV建外14号  ·  本周起,北京地铁3号线、12号线、19号线有 ... ·  20 小时前  
苏州新闻  ·  宇树科技,紧急提醒! ·  昨天  
苏州新闻  ·  宇树科技,紧急提醒! ·  昨天  
福州日报  ·  省内首个!福建这家医院的“DeepSeek医 ... ·  昨天  
51好读  ›  专栏  ›  Java基基

使用Redisson时,为何synchronized锁会失灵?

Java基基  · 公众号  · 科技自媒体  · 2025-01-06 11:55

主要观点总结

本文讨论了在高并发场景下,复制方案时遇到的并发问题,以及如何通过本地锁和分布式锁来解决这些问题。文章还介绍了@Transactional的底层实现原理和分布式锁的释放时机,并给出了正确的本地锁和分布式锁的写法。

关键观点总结

关键观点1: 并发问题和解决方案

在高并发场景下,复制方案时会出现重名问题,原因是并发操作在没有完成数据库落库的情况下,其他线程已经查询到了旧的信息。通过本地锁和分布式锁可以解决这个问题。

关键观点2: @Transactional的底层实现原理

Spring的@Transactional注解基于面向切面编程(AOP)机制实现,在方法执行前后插入事务管理相关的代码,开启事务和提交事务的时机与方法执行的情况有关。

关键观点3: 分布式锁和本地锁的释放时机

本地锁的释放时机取决于代码中的释放点,而分布式锁的释放时机由手动控制。如果提交事务和释放锁的顺序不当,可能导致并发问题。

关键观点4: 正确的写法

通过将上锁的代码放在被@Transactional注解的方法之外,确保在提交事务之后再释放锁,以避免并发问题。

关键观点5: 知识星球介绍

作者呼吁读者加入其知识星球,以提升技术能力,星球内容包括项目实战、面试招聘、源码解析、学习路线等。


正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本

来源:blog.csdn.net/wu2374633583


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

1. 并发原因

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

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

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 初步解决办法

2.1 本地锁方式

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

@Transactional(rollbackFor = Exception.class)
public  void xxxCopy(Long modelIdthrows 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 modelIdthrows GseException 
{
    RLock lock = redissonClient.getLock(REDIS_COST_MODEL_ID_LOCK + modelId);
        try {
            if (lock.tryLock(202, 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());
            }
        }
}

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. 存在的问题以及原因

问题就是以上两种写法都没有生效,但是为什么呢?

在解释这个问题之前,我们首先要弄清楚两个问题:

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

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

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

底层实现原理

  • AOP 代理: 当一个类或方法被 @Transactional 注解标记时,Spring 容器在初始化 Bean 时会检测到这个注解。对于使用 Spring 的代理模式(如 JDK 动态代理或 CGLIB),Spring 会为该 Bean 创建一个代理对象。这个代理对象会在调用实际方法前后插入事务管理相关的代码,即在方法执行前开启事务,在方法执行完毕后根据执行情况提交或回滚事务。
  • 解析注解: Spring 通过扫描 Bean 定义,识别出带有 @Transactional 注解的方法或类,并配置相应的事务属性,如传播行为、隔离级别、超时时间、是否只读等。
  • 事务拦截器: Spring 使用 AOP 机制中的拦截器( Interceptor )或 Advice (通常为 TransactionInterceptor AspectJ 的切面),在方法调用前后织入事务处理逻辑。在方法调用前,根据事务属性设置事务的开始;在方法正常结束时提交事务,如果方法抛出未检查异常(继承自 RuntimeException 的异常)或已检查异常(被 @Transactional rollbackFor 属性指定的异常)则回滚事务。

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

  • 开启事务: 事务通常在进入被 @Transactional 注解的方法之前立即开始。这意味着在执行业务逻辑之前,Spring 会确保与当前环境匹配的事务上下文已经建立。这包括选择合适的事务管理器,根据事务属性配置事务的隔离级别、传播行为等,并在数据库中实际开启事务。
  • 提交事务: 如果被注解的方法正常执行结束,没有抛出任何异常,Spring 会在离开该方法之前提交事务。提交事务意味着将所有挂起的更改永久化到数据库中,使事务中的所有操作对外可见。
  • 回滚事务: 如果在被注解的方法执行过程中抛出了异常,并且该异常未被 @Transactional noRollbackFor 属性豁免,Spring 将在捕获到异常后立即回滚事务,撤销所有在事务中已完成但未提交的操作,保持数据的一致性。

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

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

分布式锁:看你手动释放锁的时候。

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

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







请到「今天看啥」查看全文