分布式锁特性
redis 原生命令 SET的扩展命令(SET EX PX NX)
命令细节
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
code
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁 锁释放了 任务还没有走完
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁 // 机器异常了 锁没有释放
}
}
缺点:
- 锁过期释放了,业务还没执行完。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
- 锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
- 加锁的机器还没有同步到副本上的时候, 宕机了,则锁丢失了
线程A锁提前释放
线程AB公用同一个锁名字 A的锁被B删除
Lua脚本
保证SETNX + EXPIRE两条指令的原子性
redis 原生SET的扩展命令 锁的val唯一 => 解决 锁被其他线程释放问题
code
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); //释放锁
}
}
}
Lua 替代删除部分
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
Redisson框架 : 锁续命 + 锁的val唯一 => 解决自身业务没有执行完被释放的问题 + 锁被其他线程释放问题 (val = uuid+threadid)
原理: thread1 加锁成功 通过子线程续命 ; thread2 加锁失败 通过自旋锁等待加锁
问题:集群环境存在问题
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
Redisson框架 + RedLock : 锁续命 + 锁的val唯一 + 集群相对安全(使用主从依旧存在问题) => 解决 自身业务没有执行完被释放的问题 + 锁被其他线程释放问题 + 锁的master宕机
原理
- 部署多个Redis master,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。
同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。
redlock 过程:
- 获取当前时间,以毫秒为单位。
- 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
- 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
- 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
- 如果获取锁失败(没有在至少N/2+1个master实例取到锁或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。
存在问题:
- 宕机重启之后,2个客户端拿到同一把锁。 假设5个节点是A, B, C, D, E,客户端1在A, B, C上面拿到锁,D, E没有拿到锁,客户端1拿锁成功。 此时,C挂了重启,C上面锁的数据丢失(假设机器断电,数据还没来得及刷盘;或者C上面的主节点挂了,从节点未同步)。
客户端2去取锁,从C, D, E 3个节点拿到锁,A, B没有拿到(还被客户端1持有),客户端2也超过多数派,也会拿到锁。 解决方案- 延迟重启;但是由于时钟跳变的因素,导致延迟重启时效(无法解决该问题);
- 脑裂问题:就是多个客户端同时竞争同一把锁,最后全部失败。 比如有节点1、2、3、4、5,A、B、C同时竞争锁,A获得1、2,B获得3、4,C获得5,最后ABC都没有成功获得锁,没有获得半数以上的锁。
官方的建议是尽量同时并发的向所有节点发送获取锁命令。客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低。 需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,方便别的客户端去获取锁,假如释放锁失败了,就只能等待锁超时释放了
- 效率低,主节点越多,获取锁的时间越长;