来讨论一下 导致JVM本地锁失效的3种情况吧
前言
一般我们最常用的Java中的锁有 synchronized 和 ReentrantLock , synchronized是jvm内置的锁, ReentrantLock是api级别的 效率上基本差不多
因为synchronized 也已经有了不少的优化 比如锁升级操作等.., ReentrantLock会更加灵活高级
下面我会用一个扣减库存的案例来演示日常开发中需要注意的导致锁失效的方式.
首先数据库准备一个 count = 5000 的商品
无锁
下面是一个简单的扣减库存的操作 并且没有添加锁的控制
public void deduct() {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
}
ab测试并发测试一下 , 发现 count 出现了并发问题 没有扣减为 0
ab -c 100 -n 5000 http://127.0.0.1:10010/deduct
上面是因为没有锁的控制 导致并且请求进来 比如 线程1 查询商品count = 5000 线程2 也查询到商品count = 5000
都进行了 count -1 = 4999操作 , 并且都更新了数据库,从而产生了扣减库存错误问题.
添加锁
在方法上添加了 synchronized 锁 来控制这个方法每次只有一个线程执行
public synchronized void deduct() {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
//判断商品count数量 > 0 就执行扣减操作 更新数据库
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
}
把count调回到5000 后 再次 ab 并发测试一下 , 发现 count 扣减为 0
ab -c 100 -n 5000 http://127.0.0.1:10010/deduct
上面看似是没啥问题了 但是有几种情况会导致锁失效
第一种 导致锁失效情况之 多例模式
一般我们都会使用 spring框架 来做后端的web开发, 那spring中默认的bean 是 单例的, 也就是只有一个service对象
那么在使用synchronized控制方法的时候 每个线程都是拿的同一把锁 不会产生问题, 如果 service对象是 多例的,那么锁都不一样了 就无法控制并发问题了
下面演示synchronized锁 和 ReentrantLock锁 在多例模式下 失效的情况
//synchronized锁
@Service
//注意 需要指定proxyMode=TARGET_CLASS 因为使用的 jdk动态代理 这里没有写 service接口
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ProductLockService {
@Autowired private ProductLockMapper productLockMapper;
public synchronized void deduct() {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
}
}
//ReentrantLock 锁
@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ProductLockService {
@Autowired private ProductLockMapper productLockMapper;
//由于ProductLockService 是多例 所以这里的锁 也不是同一个对象了
private ReentrantLock lock = new ReentrantLock();
public void deduct() {
lock.lock();
try {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
} finally {
lock.unlock();
}
}
}
//ProductLockController
@Autowired private ProductLockService productLockService;
@GetMapping("/deduct")
public void deduct() {
log.info("productLockService: {} ", productLockService);
productLockService.deduct();
}
再次 ab 并发测试一下 , 发现 count 又出现了并发问题
这就是导致JVM本地锁失效的 第一种情况
第二种 导致锁失效情况之 事物
下面来看看 事物的使用错误 也会导致 锁失效的情况
恢复到 单例模式下 代码如下: 可以发现好像没啥问题 也是单例 也添加了锁 真的是这样吗?
...
private ReentrantLock lock = new ReentrantLock();
@Transactional
public void deduct() {
lock.lock();
try {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
} finally {
lock.unlock();
}
}
ab测试并发测试一下 , 发现 count 出现了并发问题 没有扣减为 0
这是为什么呢??? 因为添加了 @Transactional 这个方法结束后 都释放了锁后 才 commit 提交事物 , 而在锁释放后 未提交事物之前 其他线程查询到的数据 还是以前的count数量 (因为前面的线程还未提交事物)
(因为mysql 的默认隔离级别是 可重复读) 这又会导致更新重复问题 ..
那应该怎么改进
呢? 应该先执行完 事物 再释放锁, 可以把释放锁操作放到 事物方法的 后面 代码如下:
@Transactional
public void deduct() {
ProductLockEntity productLockEntity =
productLockMapper.selectOne(
new LambdaQueryWrapper<ProductLockEntity>()
.eq(ProductLockEntity::getProductCode, "1001"));
if (productLockEntity != null && productLockEntity.getCount() > 0) {
productLockEntity.setCount(productLockEntity.getCount() - 1);
productLockMapper.updateById(productLockEntity);
}
}
//调用出 添加锁
@GetMapping("/deduct")
public void deduct() {
lock.lock();
try {
productLockService.deduct();
} finally {
//此时释放锁 deduct方法已经执行完毕 并且事物已经提交了
lock.unlock();
}
}
第三种 导致锁失效情况之 集群部署
其实集群部署 和 多例模式情况差不多 , 集群部署都跨JVM了 肯定使用的锁的不一致的, 所以也会导致锁失效
这种情况我就不具体演示了, 还是很简单的, 如果有这种情况 那么可以考虑使用 分布式锁了
总结
本篇主要介绍了 导致JVM本地锁失效的3种情况, 分别为 1.多例模式 导致锁失效 2.事物 导致锁失效 3.集群部署 导致锁失效
尤其是 使用事物 的时候需要注意一下 其他的都很好理解
原文始发于微信公众号(Johnny屋):导致JVM本地锁失效的3种情况
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/89673.html