一、使用Redis中的SETNX
指令实现分布式锁
命令SET resource-name anystring NX EX max-lock-time
是一种用 Redis 来实现锁机制的简单方法
EX
seconds – 过期时间,单位秒PX
milliseconds – 过期时间,单位分钟NX
– 只有key不存在才设置key
设计思路:根据SET resource-name anystring NX EX max-lock-time
设置分布式锁,如果上述命令返回OK,那么就可以获得锁(如果返回Nil,那么在一段时间之后重新尝试),从数据库中获取数据操作成功后,通过DEL命令来释放锁。简单实现代码如下:
Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid);
if(isSet){
dataFromDb = getDataFromDb();
redisTemplate.delete("lock");//删除锁
}else{
//等待一段时间后重试上述操作,即自旋锁操作
}
}
上述代码会出现三个问题:
- 问题一:执行
getDataFromDb()
出现异常,那么后续的删除锁操作将无法执行,所以需要将redisTemplate.delete("lock")
使用try-catch-finally将异常抛出,并在finally中执行删除锁操作try{ dataFromDb = getDataFromDb(); }finally { redisTemplate.delete("lock");//删除锁 }
- 问题二:如果执行完
setIfAbsent()
即加锁完成后该进程中断导致后面代码不再执行,那么该锁将永远不会被删除,可以给锁设置一个过期时间避免这种情况。注意加锁
与设置过期时间
必须同时进行,即必须保证这两步是一个原子操作,不然当加完锁进程中断但过期时间还没设置时还是会出现问题二。加锁
与设置过期时间
在Spring可以通过setIfAbsent()
同时传入锁名和过期时间这两个参数实现。//加锁的同时设置过期时间,都设在setIfAbsent中可以保证原子性 Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
- 问题三:设置了过期时间后,如果获得锁后当前进程还没执行完时过期时间正好到了导致当前进程的锁没了,其他进程占用锁,这样当前进程执行到删除锁步骤时会把其他进程占用的锁给删了。
1) 在删除锁前可以通过value判断锁是不是当前线程的。但是获取value
和删除锁
两个操作中间如果有时间间隔还是会有可能出现问题三的情况,所以必须保证这两步的原子性。这两步的原子性可以通过lua脚本来实现。
2) 不过上述方法1)解决的是当前线程删除其他线程的锁的情况,没有从根本上解决问题,根本问题是当前线程还在执行,但是执行时锁由于过期被删,其他线程占用锁,这本身就很不合理