共计 3296 个字符,预计需要花费 9 分钟才能阅读完成。
前言
分布式锁就是在多个过程之间达到互斥的目标,常见的计划包含:基于 DB 的惟一索引、Zookeeper 的长期有序节点、Redis 的 SETNX 来实现;Redis 因为其高性能被宽泛应用,本文通过一问一答的形式来理解 Redis 如何去实现分布式锁的。
1.Redis 怎么实现分布式锁
应用 Redis
提供的 SETNX
命令保障只有一次能写入胜利
SETNX key value
当且仅当 key
不存在,则给 key
设值为 value
;若给定的key
曾经存在,则什么也不做;
127.0.0.1:6379> setnx lock 001
(integer) 1
127.0.0.1:6379> setnx lock 002
(integer) 0
当然也能够应用 SET
命令,并应用 NX
关键字
set <key> <value> NX
2. 如果获取锁的节点挂了怎么办
如果仅仅应用 SETNX
命令,当某个节点抢占到锁,如果这时候以后节点挂了,那么导致这个锁无奈开释,最终会导致死锁呈现;这时候想到的是给 key
设置一个过期工夫,这样就是节点挂了也会主动删除;
127.0.0.1:6379> expire lock 5
(integer) 1
以上应用 expire 命令设置过期工夫;
3. 如果 Set 执行完 Expire 未执行节点挂了
以上问题的起因是因为 SETNX
命令和 Expire
不是原子操作,所有有可能在执行完 SETNX
命令之后节点就挂了,这时候 Expire
还没来得及执行,同样会导致锁无奈开释,呈现死锁景象;
127.0.0.1:6379> set lock 001 ex 5 nx
OK
如上命令将 SETNX
和Expire
命令整合成一个原子操作,保障了同时胜利同时失败;
4. 没有获取锁的节点如何阻塞解决
没有获取到锁的节点须要处于阻塞状态,并且定时去重试,保障第一工夫能获取锁;
while(true){
set lock uuid ex 5 nx; ## 抢占锁
if(获取锁){break;}
......
sleep(1); ## 避免始终耗费 CPU
}
如果想性能更弱小一点能够指定阻塞工夫,超过指定阻塞工夫就间接获取锁失败;
5. 如果解决锁的可重入问题
可重入就是如果某个线程获取了锁,那么以后线程再次获取锁的时候,应该还是能够进入锁中的,每重入一次数量加一,进去时减一;本地能够应用 threadId
或者间接应用 ThreadLocal
来实现;当然最好是间接把相干信息保留在 Redis
中,Redisson
应用 lua
脚本来记录 threadId
信息:
if (redis.call('exists', KEYS[1]) == 0) then ## 如果锁不存在
redis.call('hincrby', KEYS[1], ARGV[2], 1); ## 保留锁,同时设置 threadId
redis.call('pexpire', KEYS[1], ARGV[1]); ## 设置过期工夫
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then ## 如果锁存在并且 threadId 就是以后线程 id
redis.call('hincrby', KEYS[1], ARGV[2], 1); ## 给 threadId 自增
redis.call('pexpire', KEYS[1], ARGV[1]); ## 设置过期工夫
return nil;
end; "return redis.call('pttl', KEYS[1]);
6. 如果过期工夫到了,工作刚好执行完会怎么样
失常来说咱们预估的过期工夫相对来说都比执行工作的工夫长一些,所以当工作执行完之后会做删除操作
127.0.0.1:6379> del lock
(integer) 1
有没有可能 A 节点获取的锁过期工夫到了,锁被删除,这时候 B 节点获取到锁,又从新执行了 set ex nx
命令;而刚好 A 节点工作执行实现,并且执行删除锁命令,把 B 节点的锁给删掉,呈现锁被误删的状况;
这种状况就须要咱们在删除锁的时候,查看以后被删除的锁是否就是咱们之前获取的锁,能够在 set
的时候执行一个惟一的 value
,比方间接应用uuid
;这样在删除的时候咱们须要先获取锁对应的value
值,而后和以后节点对象的 value
做比拟,统一才能够删除;
string uuid = gen(); ## 生成一个惟一 value
set lock uuid ex 5 nx; ## 抢占锁
...... ## 执行业务
string value = get lock; ## 获取以后锁对应的 value 值
if(value == uuid) { ## 比照获取的 value 值和 uuid 是否统一
del lock ## 统一执行删除操作
} else {return; ## 否则不执行删除操作}
7. 如果过期工夫到了,工作还没执行完怎么办
过期工夫是一个预估的工夫,如果真有某个工作执行的工夫很长,而这时候刚好过期工夫到了,锁就会被删除,导致其余节点又能够获取锁了,这样就呈现了多个节点同时获取锁的状况;
这种状况个别会这么解决:
- 过期工夫设置的足够长,确保工作能够执行完;
- 启动一个守护线程,为将要过期但未开释的锁减少工夫,就是给锁续命;
咱们罕用的工具包 Redisson
,外部提供了一个监控锁的看门狗,它的作用是在Redisson
实例被敞开前,一直的缩短锁的有效期;外部应用 HashedWheelTimer
作为定时器定期检查;
8.Redis 主节点宕机,还未同步从节点怎么办
咱们晓得 Redis
主从同步是异步的,如果某个节点获取了锁,这时候锁信息还未同步到从节点,主节点宕机了,从节点降级为主节点,导致锁失落;这种状况 Redis
作者提出了 redlock
算法,大抵含意如下:
在 Redis 的分布式环境中,假如咱们有 N 个 Redis 主机;这些节点是齐全独立的,因而咱们不应用复制或任何其余隐式协调系统;
当且仅当从大多数 (N/2+1,这里是 3 个节点) 的 Redis 节点都取到锁,并且应用的工夫小于锁生效工夫时,锁才算获取胜利。
Redisson
提供了 RedLock
的反对,应用也很简略:
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向 3 个 redis 实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
更多:redlock
9.Redis 呈现集群脑裂会怎么样
集群脑裂指因为网络问题,导致主节点、从节点以及 sentinel
处于不同的网络分区,因为 sentinel
的存在会因为某些主节点不存在,而晋升从节点为主节点,这时候就存在了不同的主节点,此时不同的客户端可能连贯不同的主节点,两个客户端能够同时领有同一把锁;
Redis
提供了两个配置项来限度主库的申请解决,别离是 min-slaves-to-write
和 min-slaves-max-lag
:
- min-slaves-to-write:设置了主库能进行数据同步的起码从库数量
- min-slaves-max-lag:设置了主从库间进行数据复制时,从库给主库发送
ACK
音讯的最大提早(以秒为单位)
配置项组合后要求主库连贯的从库中至多有 N 个从库、主库进行数据复制时的 ACK
音讯提早不能超过 N 秒,否则主库就不会再接管客户端的申请。
10. 如何实现一个偏心锁
咱们晓得 ReentrantLock
通过 AQS
来偏心锁,AQS
外部通过双向队列来实现,Redis 自身提供了多种数据结构包含列表、有序汇合等;Redisson
实现偏心锁正是通过 Redis 内置的数据结构来实现的:
- 应用列表作为线程的期待队列,新的期待队列增加到列表的尾部;
- 应用有序汇合寄存期待线程的程序,分数 score 是期待线程的超时工夫戳;
总结
不论应用哪种形式去实现分布式锁,咱们前提须要保障锁的性能包含:互斥性、可重入性、阻塞性;同时因为分布式的存在咱们须要保证系统的高可用、高性能、杜绝所有呈现死锁和同时取得锁的状况。