【杂谈】JPA乐观锁改悲观锁遇到的一些问题与思考
背景
接过一个外包的项目,该项目使用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
这样代码最简单。
只有当写并发高的时候,或根据业务推断可能出现高并发写操作的时候,才需考虑引入悲观锁机制。
(代码越复杂越容易出问题,越难维护)
【杂谈】JPA乐观锁改悲观锁遇到的一些问题与思考的更多相关文章
- mysql的锁--行锁,表锁,乐观锁,悲观锁
一 引言--为什么mysql提供了锁 最近看到了mysql有行锁和表锁两个概念,越想越疑惑.为什么mysql要提供锁机制,而且这种机制不是一个摆设,还有很多人在用.在现代数据库里几乎有事务机制,aci ...
- 多线程深入:乐观锁与悲观锁以及乐观锁的一种实现方式-CAS(转)
原文:https://www.cnblogs.com/qjjazry/p/6581568.html 首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每 ...
- Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...
- 乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...
- 乐观锁和悲观锁及CAS实现
乐观锁与悲观锁 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很多这种锁机制, ...
- 从事务隔离级别谈到Hibernate乐观锁,悲观锁
数据库的事务,是指作为单个逻辑工作单元执行的一系列操作. 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源.通过将一组相关操作组合为一个要么全部成功要么全部失败的单 ...
- oracle的乐观锁和悲观锁
一.问题引出 1. 假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是’有效’,表示该订单是有效的: 2. 后台管理人员查询到这条001的订单,并且看到状态 ...
- Yii2.0的乐观锁与悲观锁(转)
原文:Yii2.0的乐观锁与悲观锁 Web应用往往面临多用户环境,这种情况下的并发写入控制, 几乎成为每个开发人员都必须掌握的一项技能. 在并发环境下,有可能会出现脏读(Dirty Read).不可重 ...
- mysql中的乐观锁和悲观锁
mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ...
- Mysql共享锁、排他锁、悲观锁、乐观锁
一.相关名词 |--表级锁(锁定整个表) |--页级锁(锁定一页) |--行级锁(锁定一行) |--共享锁(S锁,MyISAM 叫做读锁) |--排他锁(X锁,MyISAM 叫做写锁) |--间隙锁( ...
随机推荐
- 和必应对话之mysql分区分表
mysql单表数据量太大 导致查询缓慢问题如何解决 您好,这是Bing.根据网络搜索结果,mysql单表数据量太大导致查询缓慢的问题有以下几种可能的解决方案: 使用索引.索引是一种数据结构,可以帮助数 ...
- js 实现仿百度换肤效果
图片自行换掉即可查看效果,原理就是基于tab切换的效果实现的 效果图 1 <!DOCTYPE html> 2 <html> 3 4 <head> 5 <met ...
- 用 Sentence Transformers v3 训练和微调嵌入模型
Sentence Transformers 是一个 Python 库,用于使用和训练各种应用的嵌入模型,例如检索增强生成 (RAG).语义搜索.语义文本相似度.释义挖掘 (paraphrase min ...
- 关于 Elasticsearch 不同分片设置的压测报告
摘要 为了验证当前集群经常出现索引超时以及请求拒绝的问题,现模拟线上集群环境及索引设置,通过压测工具随机生成测试数据,针对当前的 850 个分片的索引,以及减半之后的索引,以及更小分片索引的写入进行压 ...
- 支付宝签名和验签使用JSONObject是最优解。json字符串顺序和==符号都一致演示代码
支付宝签名和验签使用JSONObject是最优解.json字符串顺序和==符号都一致演示代码 支付宝spi接口设计验签和返回结果加签注意点,支付宝使用JSONObject对象https://www.c ...
- restful接口返回JSONObject和父类抽象实现类设计,请求头获取sign和支付宝RSA签名验签工具类方法
restful接口返回JSONObject和父类抽象实现类设计,请求头获取sign和支付宝RSA签名验签工具类方法 1.JSONObject可以通用数据的灵活性,类似Map数据,数据字段不清晰.具体返 ...
- pytest_重写pytest_sessionfinish方法的执行顺序_结合报告生成到发送邮件
背景: Python + pytest+pytest-testreport生成测试报告,到了生成报告之后,想要发送邮件,之前的方案是配合Jenkins,配置报告的路径进行发送 如果是平时的跑的项目,没 ...
- 深入理解Spring AOP中的@EnableAspectJAutoProxy
本文分享自华为云社区<Spring高手之路20--深入理解@EnableAspectJAutoProxy的力量>,作者: 砖业洋__. 1. 初始调试代码 面向切面编程(AOP)是一种编程 ...
- 牛客小白月赛97 A-D题解
AAAAAAAAAAAAAAAAAAAAA -----------------------------题解------------------------------------------- 统计数 ...
- 脚本与数据的解耦 + Page Object模型
标签(空格分隔): 脚本与数据的解耦 + Page Object模型 测试脚本和数据的解耦 你现在已经掌握了一些基本的 GUI 自动化测试用例的实现方法,是不是正摩拳擦掌准备批量开发 GUI 自动化脚 ...