关于redis:我想进大厂之分布式锁夺命连环9问-大理版人在囧途

3次阅读

共计 4278 个字符,预计需要花费 11 分钟才能阅读完成。

开个头,这是篇技术文章,然而昨天一天太恶心了,忍不住还是简略说下昨天的事件。

昨天早上 11 点飞大理,后果 9 点钟要出门的时候发现密码锁坏了,不必明码都能关上,一边司机徒弟在催着走,一边连忙打电话给房东和客服找人培修,这是第一。

而后飞机晚点,11 点 20 飞到 4 点钟才要落地,降落的过程那叫一个平稳,我认为都要没了,这也是第一次晕飞机,几乎快吐了,这是第二。

而后快 4 点了,飞机总算快要起飞了,轮子都快着地了,后果愣是拔起来又腾飞了,最初晓得是大理 8 级大风,机长不敢落地。。。这是第三。

最初告诉腾飞不晓得什么时候,要等大理那边告诉,没有方法,咱们只好下飞机转高铁,急急忙忙的一路转,总算赶上了最初 7 点前的高铁,否则就要等到 9 点当前了,最初一路周转,9 点多总算到了酒店,好在酒店还算行,没有让我太过于悲观。

这一天搞下来,整个一人在囧途,太累了。好吧,废话就这么多,文章开始。

说说分布式锁吧?

对于一个单机的零碎,咱们能够通过 synchronized 或者 ReentrantLock 等这些惯例的加锁形式来实现,然而对于一个分布式集群的零碎而言,单纯的本地锁曾经无奈解决问题,所以就须要用到分布式锁了,通常咱们都会引入三方组件或者服务来解决这个问题,比方数据库、Redis、Zookeeper 等。

通常来说,分布式锁要保障互斥性、不死锁、可重入等特点。

互斥性指的是对于同一个资源,任意时刻,都只有一个客户端能持有锁。

不死锁指的是必须要有锁超时这种机制,保障在呈现问题的时候开释锁,不会呈现死锁的问题。

可重入指的是对于同一个线程,能够多次重复加锁。

那你别离说说应用数据库、Redis 和 Zookeeper 的实现原理?

数据库的话能够应用乐观锁或者乐观锁的实现形式。

乐观锁通常就是数据库中咱们会有一个版本号,更新数据的时候通过版本号来更新,这样的话效率会比拟高,乐观锁则是通过 for update 的形式,然而会带来很多问题,因为他是一个行级锁,高并发的状况下可能会导致死锁、客户端连贯超时等问题,个别不举荐应用这种形式。

Redis 是通过 set 命令来实现,在 2.6.2 版本之前,实现形式可能是这样:

setNX命令代表当 key 不存在时返回胜利,否则返回失败。

然而这种实现形式把加锁和设置过期工夫的步骤分成两步,他们并不是原子操作,如果加锁胜利之后程序解体、服务宕机等异常情况,导致没有设置过期工夫,那么就会导致死锁的问题,其余线程永远都无奈获取这个锁。

之后的版本中,Redis 提供了原生的 set 命令,相当于两命令合二为一,不存在原子性的问题,当然也能够通过 lua 脚本来解决。

set命令如下格局:

key 为分布式锁的 key

value 为分布式锁的值,个别为不同的客户端设置不同的值

NX 代表如果要设置的 key 存在返回胜利,否则返回失败

EX 代表过期工夫为秒,PX 则为毫秒,比方下面示例中为 10 秒过期

Zookeeper 是通过创立长期程序节点的形式来实现。

  1. 当须要对资源进行加锁时,实际上就是在父节点之下创立一个长期程序节点。
  2. 客户端 A 来对资源加锁,首先判断以后创立的节点是否为最小节点,如果是,那么加锁胜利,后续加锁线程阻塞期待
  3. 此时,客户端 B 也来尝试加锁,因为客户端 A 曾经加锁胜利,所以客户端 B 发现自己的节点并不是最小节点,就会去取到上一个节点,并且对上一节点注册监听
  4. 当客户端 A 操作实现,开释锁的操作就是删除这个节点,这样就能够触发监听事件,客户端 B 就会失去告诉,同样,客户端 B 判断本人是否为最小节点,如果是,那么则加锁胜利

你说改为 set 命令之后就解决了问题?那么还会不会有其余的问题呢?

尽管 set 解决了原子性的问题,然而还是会存在两个问题。

锁超时问题

比方客户端 A 加锁同时设置超时工夫是 3 秒,后果 3s 之后程序逻辑还没有执行实现,锁曾经开释。客户端 B 此时也来尝试加锁,那么客户端 B 也会加锁胜利。

这样的话,就导致了并发的问题,如果代码幂等性没有解决好,就会导致问题产生。

锁误删除

还是相似的问题,客户端 A 加锁同时设置超时工夫 3 秒,后果 3s 之后程序逻辑还没有执行实现,锁曾经开释。客户端 B 此时也来尝试加锁,这时客户端 A 代码执行实现,执行开释锁,后果开释了客户端 B 的锁。

那下面两个问题你有什么好的解决方案吗?

锁超时

这个有两个解决方案。

  1. 针对锁超时的问题,咱们能够依据平时业务执行工夫做大抵的评估,而后依据评估的工夫设置一个较为正当的超时工夫,这样能一大部分水平上防止问题。
  2. 主动续租,通过其余的线程为将要过期的锁缩短持有工夫

锁误删除

每个客户端的锁只能本人解锁,个别咱们能够在应用 set 命令的时候生成随机的 value,解锁应用 lua 脚本判断以后锁是否本人持有的,是本人的锁能力开释。

# 加锁
SET key random_value NX EX 10
#解锁
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

理解 RedLock 算法吗?

因为在 Redis 的主从架构下,主从同步是异步的,如果在 Master 节点加锁胜利后,指令还没有同步到 Slave 节点,此时 Master 挂掉,Slave 被晋升为 Master,新的 Master 上并没有锁的数据,其余的客户端依然能够加锁胜利。

对于这种问题,Redis 作者提出了 RedLock 红锁的概念。

RedLock 的理念下须要至多 2 个 Master 节点,多个 Master 节点之间齐全相互独立,彼此之间不存在主从同步和数据复制。

次要步骤如下:

  1. 获取以后 Unix 工夫
  2. 依照程序顺次尝试从多个节点锁,如果获取锁的工夫小于超时工夫,并且超过半数的节点获取胜利,那么加锁胜利。这样做的目标就是为了防止某些节点曾经宕机的状况下,客户端还在始终期待响应后果。举个例子,假如当初有 5 个节点,过期工夫 =100ms,第一个节点获取锁破费 10ms,第二个节点破费 20ms,第三个节点破费 30ms,那么最初锁的过期工夫就是 100-(10+20+30),这样就是加锁胜利,反之如果最初工夫 <0,那么加锁失败
  3. 如果加锁失败,那么要开释所有节点上的锁

那么 RedLock 有什么问题吗?

其实 RedLock 存在不少问题,所以当初其实个别不举荐应用这种形式,而是举荐应用 Redission 的计划,他的问题次要如下几点。

性能、资源

因为须要对多个节点别离加锁和解锁,而个别分布式锁的利用场景都是在高并发的状况下,所以耗时较长,对性能有肯定的影响。此外因为须要多个节点,应用的资源也比拟多,简略来说就是费钱。

节点解体重启

比方有 1~5 号五个节点,并且没有开启长久化,客户端 A 在 1,2,3 号节点加锁胜利,此时 3 号节点解体宕机后产生重启,就失落了加锁信息,客户端 B 在 3,4,5 号节点加锁胜利。

那么,两个客户端 A\B 同时获取到了同一个锁,问题产生了,怎么解决?

  1. Redis 作者倡议的形式就是延时重启,比方 3 号节点宕机之后不要立即重启,而是期待一段时间后再重启,这个工夫必须大于锁的无效工夫,也就是锁生效后再重启,这种人为干涉的措施真正施行起来就比拟艰难了
  2. 第二个计划那么就是开启长久化,然而这样对性能又造成了影响。比方如果开启 AOF 默认每秒一次刷盘,那么最多失落一秒的数据,如果想齐全不失落的话就对性能造成较大的影响。

GC、网络提早

对于 RedLock,Martin Kleppmann 提出了很多质疑,我就只举这样一个 GC 或者网络导致的例子。(这个问题比拟多,我就不一一举例了,心里有一个概念就行了,文章地址:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

从图中咱们能够看出,client1 线获取到锁,而后产生 GC 进展,超过了锁的无效工夫导致锁被开释,而后锁被 client2 拿到,而后两个客户端同时拿到锁在写数据,问题产生。

时钟跳跃

同样的例子,假如产生网络分区,4、5 号节点变为一个独立的子网,3 号节点产生始终跳跃(不论人为操作还是同步导致)导致锁过期,这时候另外的客户端就能够从 3、4、5 号节点加锁胜利,问题又产生了。

那你说说有什么好的解决方案吗?

下面也提到了,其实比拟好的形式是应用Redission,它是一个开源的 Java 版本的 Redis 客户端,无论单机、哨兵、集群环境都能反对,另外还很好地解决了锁超时、偏心非偏心锁、可重入等问题,也实现了RedLock,同时也是官网举荐的客户端版本。

那么 Redission 实现原理呢?

加锁、可重入

首先,加锁和解锁都是通过 lua 脚本去实现的,这样做的益处是为了兼容老版本的 redis 同时保障原子性。

KEYS[1]为锁的 key,ARGV[2]为锁的 value,格局为 uuid+ 线程 ID,ARGV[1]为过期工夫。

次要的加锁逻辑也比拟容易看懂,如果 key 不存在,通过 hash 的形式保留,同时设置过期工夫,反之如果存在就是 +1。

对应的就是 hincrby', KEYS[1], ARGV[2], 1 这段命令,对 hash 构造的锁重入次数 +1。

解锁

  1. 如果 key 都不存在了,那么就间接返回
  2. 如果 key、field 不匹配,那么阐明不是本人的锁,不能开释,返回空
  3. 开释锁,重入次数 -1,如果还大于 0 那么久刷新过期工夫,反之那么久删除锁

watchdog

也叫做看门狗,也就是解决了锁超时导致的问题,实际上就是一个后盾线程,默认每隔 10 秒主动缩短锁的过期工夫。

默认的工夫就是 internalLockLeaseTime / 3internalLockLeaseTime 默认为 30 秒。

最初,理论生产中对于不同的场景该如何抉择?

首先,如果对于并发不高并且比较简单的场景,通过数据库乐观锁或者惟一主键的模式就能解决大部分的问题。

而后,对于 Redis 实现的分布式锁来说性能高,本人去实现的话比拟麻烦,要解决锁续租、lua 脚本、可重入等一系列简单的问题。

对于单机模式而言,存在单点问题。

对于主从架构或者哨兵模式,故障转移会产生锁失落的问题,因而产生了红锁,然而红锁的问题也比拟多,并不举荐应用,举荐的应用形式是用 Redission。

然而,不论抉择哪种形式,自身对于 Redis 来说不是强一致性的,某些极其场景下还是可能会存在问题。

对于 Zookeeper 的实现形式而言,自身就是保证数据一致性的,可靠性更高,所以不存在 Redis 的各种故障转移带来的问题,本人实现也比较简单,然而性能相比 Redis 稍差。

不过,理论中咱们当然是有啥用啥,老板说用什么就用什么,我才不论那么多。

正文完
 0