Redis分布式锁
今天在做Lottery分布式抽奖项目中,接触到了分布式锁这个概念,普通单机系统中,我们可以使用mutex、cas等方式来确保不同线程之间的同步和互斥,但是显然在分布式系统下,如果想让所有机器在同一时刻只有一个线程可以访问到某个共享资源,那么传统的互斥方法不再可用。这时候就需要分布式锁来解决这个问题。(分布式系统导致出来的问题真的好多~开发难度极具提高)
分布式锁的特性
- 「互斥性」: 任意时刻,只有一个客户端能持有锁。
- 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
- 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
- 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
- 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
Redis分布式锁方案
所有的方案本质都是使用redis的setnx(Set not exist) 和expire(设置过期时间来实现)两个命令来实现
方案一:
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
存在问题:setnx和expire命令分开,不是原子操作。如果setnx后,但是没有设置expire前,进程被关闭,别的线程将永远获取不到这个锁。
方案二
为了解决方案一中的问题,有个解决思路就是将过期时间设置在value值当中,避免使用expire来设置过期时间。
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
// 类似cas的方法来考虑多线程并发问题,只有一个线程的设置值和当前值相同,它才可以加锁
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
但是该方法也存在问题:
- 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
- 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖
方案三:
使用Lua脚本来保证原子性(现在还没有学习过Lua脚本),之后有空可以学习一下。
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end;
加锁的代码如下:
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);
这种方案很简单,写一个Lua脚本即可。
方案四:
使用set的扩展命令,简单来说就是把原子性交给redis来实现。通过一条set命令即设置好过期时间,设置好NX
格式如下:
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
到此,上面的方案可能都会存在的问题:
锁设置的时间太短,导致业务还没有完成,其它线程去获取锁就拿到了分布式锁,导致临界区的代码不能互斥运行。比如:线程A设置过期时间100ms,业务由于某些原因运行了200ms,那么其它线程再100-200ms之间可以拿到分布式锁,且当线程A运行结束,会去释放锁,从而释放掉了其它线程拥有的锁。
方案五:
解决上述方案中存在的问题,方法是通过value值设置未一个标记当前线程唯一的随机数,做一个校验即可。
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
上述代码存在的问题,判断相等和释放锁属于两个操作,依旧可能存在判断完成后,但没有执行del时,锁被其它线程占用,导致释放掉了其它线程的锁。解决方法也简单,就是使用lua脚本
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
方案六
最后的问题就是如何解决锁过期释放,业务没有执行完的问题。显然,如果只是单纯设置超过业务执行时间的时间,是很容易出现问题的,因为我们不能确保业务在多久内可以执行结束,如果盲目设置过大的过期时间,会导致锁的性能过差。
在redisson框架中,使用了看门狗机制,每当线程加锁成功,会创建一个后台线程,每隔一段时间检查一下,如果锁还被当前线程持有,就去延长时间,直到业务完成释放锁。
标签:过期,Redis,线程,key,分布式,id,客户端 From: https://www.cnblogs.com/xyfhsy/p/17980163