关于java:Redis实现分布式锁十连问

47次阅读

共计 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

如上命令将 SETNXExpire命令整合成一个原子操作,保障了同时胜利同时失败;

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-writemin-slaves-max-lag

  • min-slaves-to-write:设置了主库能进行数据同步的起码从库数量
  • min-slaves-max-lag:设置了主从库间进行数据复制时,从库给主库发送 ACK 音讯的最大提早(以秒为单位)

配置项组合后要求主库连贯的从库中至多有 N 个从库、主库进行数据复制时的 ACK 音讯提早不能超过 N 秒,否则主库就不会再接管客户端的申请。

10. 如何实现一个偏心锁

咱们晓得 ReentrantLock 通过 AQS 来偏心锁,AQS外部通过双向队列来实现,Redis 自身提供了多种数据结构包含列表、有序汇合等;Redisson实现偏心锁正是通过 Redis 内置的数据结构来实现的:

  • 应用列表作为线程的期待队列,新的期待队列增加到列表的尾部;
  • 应用有序汇合寄存期待线程的程序,分数 score 是期待线程的超时工夫戳;

总结

不论应用哪种形式去实现分布式锁,咱们前提须要保障锁的性能包含:互斥性、可重入性、阻塞性;同时因为分布式的存在咱们须要保证系统的高可用、高性能、杜绝所有呈现死锁和同时取得锁的状况。

正文完
 0