经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
【杂谈】JPA乐观锁改悲观锁遇到的一些问题与思考
来源:cnblogs  作者:猫毛·波拿巴  时间:2024/7/31 15:12:42  对本文有异议

背景

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

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

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

原理知识

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

  1. 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已被修改。

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

需求

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

处理——重试

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

案例代码

修改前

  1. @Service
  2. public class ProductService {
  3. @Autowired
  4. private ProductRepository productRepository;
  5. @Transactional
  6. public void updateProductPrice(Long productId, Double newPrice) {
  7. Product product = productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
  8. product.setPrice(newPrice);
  9. productRepository.save(product);
  10. }
  11. }

修改后

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

  1. @Service
  2. public class ProductService {
  3. @Autowired
  4. private ProductRepository productRepository;
  5. public void updateProductPriceWithRetry(Long productId, Double newPrice) {
  6. boolean updated = false;
  7. //一直重试直到成功
  8. while(!updated) {
  9. try {
  10. updateProductPrice(productId, newPrice);
  11. updated = true;
  12. } catch (OpitimisticLockingFailureException e) {
  13.            System.out.println("updateProductPrice lock error, retrying...")
  14. }
  15. }
  16.    }
  17. @Transactional
  18. public void updateProductPrice(Long productId, Double newPrice) {
  19. Product product = productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
  20. product.setPrice(newPrice);
  21. productRepository.save(product);
  22. }
  23. }

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

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

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

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

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

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

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

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

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

  1. @Service
  2. public class ProductService {
  3. @Autowired
  4. private ProductRepository productRepository;
  5. public void someComplicateOperationWithLock(Object params) {
  6. //该业务涉及到的几个对象修改,需要获得该对象的锁
  7. //key=类前缀+对象id
  8. List<String> keys = Arrays.asList(....);
  9. //RedisLockUtil为分布式锁,可自行封装(可基于redisson实现)
  10. //获得锁之后才开始执行任务代码,然后在任务执行结束释放锁
  11. RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):
  12. }
  13. @Transactional
  14. public void someComplicateOperation(Object params) {
  15. .....
  16. }
  17. }

遇到的坑

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

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

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

案例:

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

应对方式——refresh

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

  1. @Service
  2. public class ProductService {
  3. @Autowired
  4. private ProductRepository productRepository;
  5. public void someComplicateOperationWithLock(Object params) {
  6. //获得锁之前先查询了一次,此次查询数据将缓存在持久化上下文中
  7. String productId = xxxx;
  8. Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));
  9. //该业务涉及到的几个对象修改,需要获得该对象的锁
  10. //key=类前缀+对象id
  11. List<String> keys = Arrays.asList(....);
  12. //RedisLockUtil为分布式锁,可自行封装
  13. //获得锁之后才开始执行任务代码,然后在任务执行结束释放锁
  14. RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):
  15. }
  16. @Transactional
  17. public void someComplicateOperation(Object params) {
  18. .....
  19. //取到缓存内的旧数据
  20. Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));
  21. //使用refresh方法,强制从数据库捞取最新数据,并更新到持久化上下文中
  22. EntityManager entityManager = SpringUtil.getBean(EntityManager.class)
  23. product = entityManager.refresh(product);
  24. ....
  25. }
  26. }

总结

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

关于一致性保护

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

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

这样代码最简单。

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

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

原文链接:https://www.cnblogs.com/longfurcat/p/18334599

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号