https://blog.csdn.net/zhw21w/article/details/129563500
一说到实现Redis
的分布式锁,很多小伙伴马上就会想到setnx+ expire
命令。也就是说,先用setnx
来抢锁,如果抢到之后,再用expire
给锁设置一个过期时间。
伪代码如下:
if(jedis.setnx(lock_key,lock_value) == 1){ //加锁
jedis.expire(lock_key,timeout); //设置过期时间
doBusiness //业务逻辑处理
}
这块代码是有坑的,因为setnx
和expire
两个命令是分开写的,并不是原子操作!如果刚要执行完setnx
加锁,正要执行expire
设置过期时间时,进程crash
或者要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁啦。
为了解决:发生异常时,锁得不到释放的问题。有小伙伴提出,可以把过期时间放到setnx
的value
里面。如果加锁失败,再拿出value
值和当前系统时间校验一下是否过期即可。伪代码实现如下:
long expireTime = System.currentTimeMillis() + timeout; //系统时间+设置的超时时间
String expireTimeStr = String.valueOf(expireTime); //转化为String字符串
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lock_key, expireTimeStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String oldExpireTimreStr = jedis.get(lock_key);
// 如果获取到的老的预期过期时间,小于系统当前时间,表示已经过期了
if (oldExpireTimreStr != null && Long.parseLong(oldExpireTimreStr) < System.currentTimeMillis()) {
//锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(lock_key, expireTimeStr);
if (oldValueStr != null && oldValueStr.equals(oldExpireTimreStr)) {
//考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
这种实现的方案,也是有坑的:如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet()
,最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
之前review
代码的时候,看到这样实现的分布式锁,伪代码:
try{
if(jedis.setnx(lock_key,lock_value) == 1){//加锁
doBusiness //业务逻辑处理
return true; //加锁成功,处理完业务逻辑返回
}
return false; //加锁失败
} finally {
unlock(lockKey);- //释放锁
}
这块有什么问题呢?是的,忘记设置过期时间了。如果程序在运行期间,机器突然挂了,代码层面没有走到finally
代码块,即在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以这里需要给lockKey
加一个过期时间。注意哈,使用分布式锁,一定要设置过期时间哈。
很多小伙伴,会使用Redis
的set
指令扩展参数来实现分布式锁。
set指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,
而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
小伙伴会写出如下伪代码: