首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

【杂谈】JPA乐观锁改悲观锁遇到的一些问题与思考

编程知识
2024年07月31日 14:12

背景

接过一个外包的项目,该项目使用JPA作为ORM。

项目中有多个entity带有@version字段

当并发高的时候经常报乐观锁错误OptimisticLocingFailureException

原理知识

JPA的@version是通过在SQL语句上做手脚来实现乐观锁的

UPDATE table_name SET updated_column = new_value, version = new_version WHERE id = entity_id AND version = old_version

这个"Compare And Set"操作必须放到数据库层,数据库层能够保证"Compare And Set"的原子性(update语句的原子性)

如果这个"Compare And Set"操作放在应用层,则无法保证原子性,即可能version比较成功了,但等到实际更新的时候,数据库的version已被修改。

这时候就会出现错误修改的情况

需求

解决此类报错,让事务能够正常完成

处理——重试

既然是乐观锁报错,那就是修改冲突了,那就自动重试就好了

案例代码

修改前

@Service
public class ProductService {
       
     @Autowired
     private ProductRepository productRepository;

     @Transactional
     public void updateProductPrice(Long productId, Double newPrice) {
          Product product = productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
          product.setPrice(newPrice);
          productRepository.save(product);
     }   
}

修改后

增加一个withRetry的方法,对于需要保证修改成功的地方(比如用户在UI页面上的操作),可以调用此方法。

@Service
public class ProductService {
       
     @Autowired
     private ProductRepository productRepository;

     public void updateProductPriceWithRetry(Long productId, Double newPrice) {
         boolean updated = false;
          //一直重试直到成功
          while(!updated) {
               try {
                   updateProductPrice(productId, newPrice);
                   updated = true;
               } catch (OpitimisticLockingFailureException e) {
           System.out.println("updateProductPrice lock error, retrying...")
               }
          } 
   }

     @Transactional
     public void updateProductPrice(Long productId, Double newPrice) {
          Product product = productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
          product.setPrice(newPrice);
          productRepository.save(product);
     }   
} 

依赖乐观锁带来的问题——高并发带来高冲突

上面的重试能够解决乐观锁报错,并让业务操作能够正常完成。但是却加重了数据库的负担。

另外乐观锁也有自己的问题:

业务层将事务修改直接提交给数据库,让乐观锁机制保障数据一致性

这时候并发越高,修改的冲突就更多,就有更多的无效提交,数据库压力就越大

高冲突的应对方式——引入悲观锁

解决高冲突的方式,就是在业务层引入悲观锁。

在业务操作之前,先获得锁。

一方面减少提交到数据库的并发事务量,另一方面也能减少业务层的CPU开销(获得锁后才执行业务代码)

@Service
public class ProductService {
       
     @Autowired
     private ProductRepository productRepository;

     
     public void someComplicateOperationWithLock(Object params) {
          
          //该业务涉及到的几个对象修改,需要获得该对象的锁
          //key=类前缀+对象id
          List<String> keys = Arrays.asList(....);
          
          //RedisLockUtil为分布式锁,可自行封装(可基于redisson实现)
          //获得锁之后才开始执行任务代码,然后在任务执行结束释放锁
          RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):
    
     }
  

     @Transactional
     public void someComplicateOperation(Object params) {
         .....
     }   
}    

遇到的坑

正常在获得锁之后,需要重新加载最新的数据,这样修改的时候才不会冲突。(前一个锁获得者可能修改了数据)

但是,JPA有持久化上下文,有一层缓存。如果在获得锁之前就将对象捞了出来,等获得锁之后重新捞还会得到缓存内的数据,而非数据库最新数据。

这样的话,即使用了悲观锁,事务提交的时候还是会出现冲突。

案例:

@Service
public class ProductService {
       
     @Autowired
     private ProductRepository productRepository;

     
     public void someComplicateOperationWithLock(Object params) {
//获得锁之前先查询了一次,此次查询数据将缓存在持久化上下文中 String productId
= xxxx; Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found")); //该业务涉及到的几个对象修改,需要获得该对象的锁 //key=类前缀+对象id List<String> keys = Arrays.asList(....); //RedisLockUtil为分布式锁,可自行封装 //获得锁之后才开始执行任务代码,然后在任务执行结束释放锁 RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}): } @Transactional public void someComplicateOperation(Object params) { ..... //取到缓存内的旧数据 Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found")); .... } }

应对方式——refresh

在悲观锁范围内,首次加载entity数据的时候,使用refresh方法,强制从DB捞取最新数据。

@Service
public class ProductService {
       
     @Autowired
     private ProductRepository productRepository;

     
     public void someComplicateOperationWithLock(Object params) {
          //获得锁之前先查询了一次,此次查询数据将缓存在持久化上下文中
          String productId = xxxx;
          Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));
          
          //该业务涉及到的几个对象修改,需要获得该对象的锁
          //key=类前缀+对象id
          List<String> keys = Arrays.asList(....);
          
          //RedisLockUtil为分布式锁,可自行封装
          //获得锁之后才开始执行任务代码,然后在任务执行结束释放锁
          RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):
    
     }
  

     @Transactional
     public void someComplicateOperation(Object params) {
         .....
         //取到缓存内的旧数据
         Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));
        //使用refresh方法,强制从数据库捞取最新数据,并更新到持久化上下文中
        EntityManager entityManager = SpringUtil.getBean(EntityManager.class)
        product = entityManager.refresh(product);
         ....
     }   
}    

总结

此项目采用乐观锁+悲观锁混合方式,用悲观锁限制并发修改,用乐观锁做最基本的一致性保护。

关于一致性保护

对于一些简单的应用,写并发不高,事务+乐观锁就足够了

  • entity里面加一个@version字段
  • 业务方法加上@Transactional

这样代码最简单。

只有当写并发高的时候,或根据业务推断可能出现高并发写操作的时候,才需考虑引入悲观锁机制。 

(代码越复杂越容易出问题,越难维护)

From:https://www.cnblogs.com/longfurcat/p/18334599
本文地址: http://shuzixingkong.net/article/631
0评论
提交 加载更多评论
其他文章 探索Amazon S3:存储解决方案的基石(Amazon S3使用记录)
探索Amazon S3:存储解决方案的基石 本文为上一篇minio使用的衍生版 相关链接:1.https://www.cnblogs.com/ComfortableM/p/18286363 ​ 2.https://blog.csdn.net/zizai_a/article/details/14079
探索Amazon S3:存储解决方案的基石(Amazon S3使用记录) 探索Amazon S3:存储解决方案的基石(Amazon S3使用记录) 探索Amazon S3:存储解决方案的基石(Amazon S3使用记录)
ComfyUI插件:ComfyUI layer style 节点(一)
前言: 学习ComfyUI是一场持久战,而ComfyUI layer style 是一组专为图片设计制作且集成了Photoshop功能的强大节点。该节点几乎将PhotoShop的全部功能迁移到ComfyUI,诸如提供仿照Adobe Photoshop的图层样式、提供调整颜色功能(亮度、饱和度、对比度
ComfyUI插件:ComfyUI layer style 节点(一) ComfyUI插件:ComfyUI layer style 节点(一) ComfyUI插件:ComfyUI layer style 节点(一)
我的编程经历,从天桥地摊Basic到西藏阿里的.Net AOT。(一,从井到Sharp)
小霸王学习机附带有basic语言, 我想当然的打了一个 print 'what's your name?' ,它却没有给我期望的答案,直到16年后才由 siri 给出了回答。1995年,《电子游戏软件》做了三期连载,叫《世嘉五代与超级任天堂的对比报告》
我的编程经历,从天桥地摊Basic到西藏阿里的.Net AOT。(一,从井到Sharp)
Jmeter二次开发函数 - 文本替换
此篇文章将在Jmeter创建一个新函数,实现替换文本中的指定内容功能。效果图如下 1、eclipse项目创建步骤此处省略,可参考上一篇Jmeter二次开发函数之入门 2、新建class命名为“TextReplaceFunction”,并继承jmeter自带的AbstractFunction 3、新生
Jmeter二次开发函数 - 文本替换 Jmeter二次开发函数 - 文本替换 Jmeter二次开发函数 - 文本替换
nacos配置&gateway配置服务发现一直报500
项目场景: 这两天不是一直在搞简化配置、使用公共配置、我的服务可以通过网关访问这几个任务嘛,也是不断地踩坑补知识才总算把这几个任务都搞好了,下面就是记录过程中遇到的问题。 使用公共配置 因为发现项目使用的配置文件过多,有application、application-test.yml、bootstr
SQL实战从在职到离职(1) 如何处理连续查询
书接上回,最近离职在家了实在无聊,除了看看考研的书,打打dnf手游,也就只能写写代码,结果昨晚挂在某平台的一个技术出售有人下单了,大概业务是需要帮忙辅导一些面试需要用到的SQL。 回想了下,在该平台接单SQL也超过3w元了,考察的也就是那几大类,我准备开一个新的专题,把我遇到的题目做一些示例和总结,
SQL实战从在职到离职(1) 如何处理连续查询
结合拦截器描述mybatis启动流程
简介 mybatis的启动入口一般有两个,在结合spring框架后由spring整合包下的SqlSessionFactoryBean启动 如果没有整合spring,则有XMLConfigBuilder启动 这两个启动入口都会初始化Configuration对象,该对象是mybatis配置文件的对象形
结合拦截器描述mybatis启动流程
概述C#中各种类型集合的特点
在C#中,集合是用于存储和操作一组数据项的数据结构。这些集合通常位于 System.Collections 和 System.Collections.Generic 命名空间中。下面我将概述C#中几种常用的集合类型及其特点: 1. System.Collections 命名空间中的集合 这个命名空间
概述C#中各种类型集合的特点