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

昨天早上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 0end

理解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稍差。

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