Redis - 实现分布式锁的阶段演进
程序员文章站
2024-02-03 18:17:10
...
①演进阶段一
获得锁就执行业务逻辑,没有获得锁就继续调用这个方法形成一个自旋,就类似于synchronized
。
伪代码:
public void getData(){
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
if(lock){
// 执行业务..
// 删除锁
redisTemplate.delete("lock");
}else{
// 休眠一段时间
// 继续调用getData,等待锁的释放
getData();
}
}
存在问题:
setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
。
解决方法:
给锁设置一个过期时间
,即使代码异常或是程序宕机,锁都会因为过期时间到了自动删除。
②演进阶段二
解决阶段一的问题。
伪代码:
public void getData(){
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
if(lock){
// 设置过期时间
redisTemplate.expire("lock",180,TimeUnit.SECONDS);
// 执行业务..
// 删除锁
redisTemplate.delete("lock");
}else{
// 休眠一段时间
// 继续调用getData,等待锁的释放
getData();
}
}
存在问题:
获得锁之后,正要去设置过期时间,这是服务宕机/断电,这个时间还没设置上去,又导致了死锁。
解决方法:
这就需要保证设置value和过期时间是原子性操作,这就需要使用Redis的setnx ex命令。
原来将键key设定为指定的“字符串”值,如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。
在2.6.12版本开始,redis为SET命令增加了一系列选项:
EX seconds 设置键key的过期时间,单位时秒
PX milliseconds 设置键key的过期时间,单位时毫秒
NX 只有键key不存在的时候才会设置key的值
XX 只有键key存在的时候才会设置key的值
命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
③演进阶段三
解决阶段二的问题。
伪代码:
public void getData(){
// 设置了过期时间 - 180s
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
if(lock){
// 执行业务...
// 删除锁
redisTemplate.delete("lock");
}else{
// 休眠一段时间
// 继续调用getData,等待锁的释放
}
}
存在问题:
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
场景:
比如我们执行一个业务,线程1拿到了Redis锁,Redis锁过期时间是180s,我们这个业务却执行了300s,也就是说,我们执行到180s的时候,锁已经过期了,那另外一个线程2拿到了锁,等我们线程1业务执行完的时候,线程2业务还在执行中,线程1要去删锁,这时删除的锁其实是线程2的。
解决方法:
使用随机的大字符串作为value值,删除前先对比value值是否相同,相同就删除。
④演进阶段四
解决阶段三的问题。
伪代码:
public void getData(){
// 使用UUID生成不重复值
String uuid = UUID.randomUUID().toString();
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
// 执行业务...
// 判断uuid是否相同
String str = redisTemplate.get("lock");
if(uuid.equals(str){ // 相同,就删除锁
redisTemplate.delete("lock");
}
}else{
// 休眠一段时间
// 继续调用getData,等待锁的释放
}
}
解决方法:
保证对比value值和删除value值是一个原子性操作,使用Redis+Lua脚本来完成。
⑤演进阶段五
解决阶段四的问题,最终形态。
伪代码:
public void getData(){
// 使用UUID生成不重复值
String uuid = UUID.randomUUID().toString();
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
try{
// 执行业务...
}finally{
// Lua脚本 - 2个参数 key和value
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
}
}else{
// 休眠一段时间
// 继续调用getData,等待锁的释放
}
}
存在问题:
锁的过期时间,如果业务没执行完,锁应该续期,最简单的解决方法就是把锁的过期时间设置大一点。
分布式锁框架
分布式锁框架Redisson,基于Redis实现的各种分布式锁。
Redisson的GitHub地址