首页 > 其他分享 >【杂谈】JPA乐观锁改悲观锁遇到的一些问题与思考

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

时间:2024-07-31 15:21:26浏览次数:6  
标签:Product JPA void 杂谈 product productRepository 锁改 public productId

背景

接过一个外包的项目,该项目使用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

这样代码最简单。

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

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

标签:Product,JPA,void,杂谈,product,productRepository,锁改,public,productId
From: https://www.cnblogs.com/longfurcat/p/18334599

相关文章

  • DevOps - DevOps随想杂谈
    1-趋势与本义随着技术的发展,基础设施和应用程序之间的界限会变得越来越模糊,"服务"管理也将变得更加全面和简单。通过实施DevOps可以便捷地搭建包含交付流水线的研发协作平台,可以快速实现商业价值。在这一过程中,反对将DevOps绝对理论化、模型化,而是坚持DevOps的实践性和......
  • 杂谈-iOS马甲包
    什么是马甲包马甲包一般是主APP的分身或者克隆,也或者说是穿着马甲的一个APP,脱掉马甲,APP将呈现另一种样式,也就是常说的AB面APP。1、主APP的分身或者克隆类型的马甲包先说第一种就是主APP的分身或者克隆,现在很公司一般有一个自己的主产品,但是也会去做一些和主APP类似的阉割版......
  • 使用Java和JPA构建健壮的数据库应用
    使用Java和JPA构建健壮的数据库应用大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!今天,我们将探讨如何使用Java和JPA(JavaPersistenceAPI)来构建健壮的数据库应用。JPA是JavaEE规范的一部分,用于对象关系映射(ORM),简化了数据库操作和数据管理。1.JPA基础......
  • 杂谈:Vue 的 Diff 算法
    Vue.js使用虚拟DOM来高效地更新用户界面,其中的Diff算法是关键。Diff算法负责找出新旧虚拟DOM之间的差异,并高效地更新实际DOM。本文将详细解析Vue的Diff算法的工作原理和在实际开发中的应用。1.什么是虚拟DOM虚拟DOM是一个轻量级的JavaScript对象,用于描述DOM......
  • jpa报错 Failed to initialize JPA EntityManagerFactory: Unable to instantiate de
    报错2024-07-1711:18:57.558[][main]o.h.dialect.Dialect:HHH000400:Usingdialect:org.hibernate.dialect.MySQL5InnoDBDialect2024-07-1711:18:57.729[][main]tyManagerFactoryBean:FailedtoinitializeJPAEntityManagerFactory:......
  • OpenVX生命周期杂谈
    OpenVX生命周期杂谈1.OpenVX上下文生命周期OpenVX上下文的生命周期非常简单,如图2-7所示。 图2-7.OpenVX上下文的生命周期模型2.图形生命周期如图2-8所示,OpenVX在图形生命周期中有四个主要阶段。1)构造:图形通过vxCreateGraph创建,节点通过数据对象连接在一起。2)验证:检查......
  • 使用Spring Data JPA实现持久化层的简化开发
    使用SpringDataJPA实现持久化层的简化开发大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在现代的Java应用开发中,SpringDataJPA为我们提供了一种简单而强大的方式来操作数据库,本文将深入探讨如何利用SpringDataJPA简化持久化层的开发。一、Spring......
  • 使用Spring Data JPA进行数据库操作
    使用SpringDataJPA进行数据库操作大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!1.简介SpringDataJPA是Spring框架提供的一种用于简化数据库操作的技术,基于JPA(JavaPersistenceAPI)规范,通过简单的接口和方法,可以轻松地实现对数据库的增删改查操作......
  • TDA4VM-SK配置与应用杂谈
    TDA4VM-SK配置与应用杂谈硬件信息:SK-TDA4VM用户指南处理器SDKLinux边缘AI文档配置文档:SK-TDA4VM处理器SDKLinux文档-getting_started,详细说明了如何配置,下面是简要步骤:物料准备:SK板,microUSB串口线,USBcamera,HDMI/DP显示器,≥16GB的内存卡,网线和局域网*,串口电源(5-2......
  • 【Springboot】玩转复杂单元测试启动类-只测试数据访问层(JPA+Mybatis) 和服务层 以及
    上一篇文章写了一个最复杂的SpringBootTest启动类,定制化程序奇高,然而有时候仅测试JPA是不够的。启动类需求:测试SpringDataJPA测试Mybatis从容器中获得ObjectMapper测试单独的Service使用TestNG或者使用Junit阻止Dubbo、Kafka、ElasticSearch等中间件启动使用appl......