之前介绍分布式锁的时候,给过一个漫画介绍分布式锁的链接,传送门——漫画说明分布式锁这篇总结写的确实不错。但是有些地方我们可以按照其他思路来进行实现。
这里先需要了解一下各种失败机制,failover,failsafe,failfast,failback,forking。
如果要实现一个锁的机制,无非就是加锁,释放锁,已经其他异常场景下的几种处理。在redis下如何实现这些操作还是值得探讨的。
老版本的redis可以通过setnx命令来实现分布式锁(本篇博客也会采用这种方式)setnx命令文档,这个命令在key已经存在的情况下会返回0,在key不存在的情况下才会返回1,至于key所对应的value,这个根据业务随意就好。
解锁就很简单了,直接del指定的key值就可以了
如果一个得到锁的线程在执行任务的过程中遇到了异常,来不及显示的释放分布式锁,则这个资源就会被永远锁住,这个是要处理的。可以给redis中指定的key设置一个过期时间,就可以解决这一问题,不过个人觉得设置一个标志位似乎更加合理一点。
在高一点的redis版本中可以通过set命令设置过期时间。
在设置了过期时间之后,如果一个进程A执行很慢,在规定时间中没有顺利执行完指定的业务逻辑代码,锁就被释放了,之后进程B获得了锁(这个锁key值相同,但是value值不同)。进程B执行一段时间之后,进程A如果没有做value值的判断,则会删除进程B的分布式锁。因此在释放锁的时候,需要判断一下value值。
这里只是简单的列举了两个问题,造成这些问题的原因无非就一个——加锁/解锁+业务操作的原子性。
先上一个简单的版本,我们利用setNx获取锁。
/*
* 基于redis的分布式锁
* @param productLockDto
* @return
*/
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto){
int result = 0;
final String key = String.format("redis_lock_product_id:%s",productLockDto.getId());
String value = UUID.randomUUID().toString()+System.nanoTime();
//利用setNx操作建立锁。
Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
if(res){
//开始核心的业务逻辑处理
ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
if(productLockEntity!=null && productLockEntity.getStock().compareTo(productLockDto.getStock())>=0){
productLockEntity.setStock(productLockDto.getStock());
//与普通的更新操作相比,仅仅是增加了版本号的统计
result = lockMapper.updateStockForNegative(productLockEntity);
if(result>0){
log.info("通过redis的分布式锁更新成功,剩余库存stock={}",productLockDto.getStock());
}
}
}
return result;
}
但是比较恶心的是,这里没有释放锁的操作,因此只能获取一次锁,然后操作一次,之后并没有释放锁,导致系统只能更新一次数据。日志如下所示:
数据库中也只是显示更新一次
上述的代码中没有释放锁的操作,导致是有一个进程能对数据进行操作,但是在整体介绍部分说过,在释放锁之前需要对比一下redis锁的key值,避免出现进程A删除进程B对应的分布式锁的key值。
/*
* 基于redis的分布式锁
* @param productLockDto
* @return
*/
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto){
int result = 0;
final String key = String.format("redis_lock_product_id:%s",productLockDto.getId());
String value = UUID.randomUUID().toString()+System.nanoTime();
//利用setNx操作建立锁。
Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
try{
if(res){
//真正的更新操作
ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
if(productLockEntity!=null && productLockEntity.getStock().compareTo(productLockDto.getStock())>=0){
productLockEntity.setStock(productLockDto.getStock());
//与普通的更新操作相比,仅仅是增加了版本号的统计
result = lockMapper.updateStockForNegative(productLockEntity);
if(result>0){
log.info("通过redis的分布式锁更新成功,剩余库存stock={}",productLockEntity.getStock());
}
}
}
//失败了就失败了,这里什么都没做。
}catch (Exception e){
log.error("出现异常,异常信息为:{}",e.fillInStackTrace());
}finally {//无论如何,这里都需要释放锁。
String redisValue = stringRedisTemplate.opsForValue().get(key);
if(value.equals(redisValue)){//之前分析过,为了避免出现进程A删掉进程B的Key,这里需要做一个判断
stringRedisTemplate.delete(key);
}
}
return result;
}
引入了try-finally语句块,这样就能保证在任何时候都能释放锁,在释放锁的时候进行了一个必要的判断,这样能正确处理数据。同样用jmeter模拟2000个线程之后,数据如下。
锁正确释放之后,多个进程能进行数据操作。
2000个进程,最终只是500多个进程抢到了锁,因为我们采取了failover的机制。有些业务场景中,我们需要让这些获取锁失败的进程进行一个轮询,然后再次加入锁的竞争中。
先直接上实例吧
/*
* 基于redis的分布式锁
*
* @param productLockDto
* @return
*/
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto) {
int result = 0;
final String key = String.format("redis_lock_product_id:%s", productLockDto.getId());
Boolean res = true;//利用一个标志位。根据标志位不断轮询判断
while (res) {
String value = UUID.randomUUID().toString() + System.nanoTime();
//利用setNx操作建立锁。
res = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if (res) {
try {
res = false;//将标志位置为false,为了后续跳出循环
//真正的更新操作
ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
int recordStock = productLockEntity.getStock();
if (productLockEntity != null && productLockEntity.getStock().compareTo(productLockDto.getStock()) >= 0) {
productLockEntity.setStock(productLockDto.getStock());
//与普通的更新操作相比,仅仅是增加了版本号的统计
result = lockMapper.updateStockForNegative(productLockEntity);
if (result > 0) {
log.info("通过redis的分布式锁更新成功,剩余库存stock={}", recordStock - 1);
}
}
} catch (Exception e) {
log.error("出现异常,异常信息为:{}", e.fillInStackTrace());
} finally {
String redisValue = stringRedisTemplate.opsForValue().get(key);
if (value.equals(redisValue)) {//之前分析过,为了避免出现进程A删掉进程B的Key,这里需要做一个判断
stringRedisTemplate.delete(key);
}
}
}else{
res=true;//让获取分布式锁失败的线程继续获取锁。
}
}
return result;
}
这个版本中提现了轮询的思想,这个在JDK中可重入锁的源码中有很多体现。这里不再赘述,具体数据结果如下(2000个线程,需要适当扩大数据库连接池的配置)
可以看到每个请求均正常获取到相关数据。模拟的2000个线程均正常更新了数据。
本篇博客简单总结了redis中分布式锁的实践,篇实战,没啥可总结的。