前言
分布式锁就是在多个过程之间达到互斥的目标,常见的计划包含:基于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 <span style="color:transparent">来源gaodai#ma#com搞*!代#%^码网</span>= 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
的反对,应用也很简略:
<code class="java">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是期待线程的超时工夫戳;
总结
不论应用哪种形式去实现分布式锁,咱们前提须要保障锁的性能包含:互斥性、可重入性、阻塞性;同时因为分布式的存在咱们须要保证系统的高可用、高性能、杜绝所有呈现死锁和同时取得锁的状况。