开个头,这是篇技术文章,然而昨天一天太恶心了,忍不住还是简略说下昨天的事件。
昨天早上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 为分布式锁的keyvalue 为分布式锁的值,个别为不同的客户端设置不同的值
NX 代表如果要设置的key存在返回胜利,否则返回失败
EX 代表过期工夫为秒,PX则为毫秒,比方下面示例中为10秒过期
Zookeeper是通过创立长期程序节点的形式来实现。
- 当须要对资源进行加锁时,实际上就是在父节点之下创立一个长期程序节点。
- 客户端A来对资源加锁,首先判断以后创立的节点是否为最小节点,如果是,那么加锁胜利,后续加锁线程阻塞期待
- 此时,客户端B也来尝试加锁,因为客户端A曾经加锁胜利,所以客户端B发现自己的节点并不是最小节点,就会去取到上一个节点,并且对上一节点注册监听
- 当客户端A操作实现,开释锁的操作就是删除这个节点,这样就能够触发监听事件,客户端B就会失去告诉,同样,客户端B判断本人是否为最小节点,如果是,那么则加锁胜利
你说改为set命令之后就解决了问题?那么还会不会有其余的问题呢?
尽管set
解决了原子性的问题,然而还是会存在两个问题。
锁超时问题
比方客户端A加锁同时设置超时工夫是3秒,后果3s之后程序逻辑还没有执行实现,锁曾经开释。客户端B此时也来尝试加锁,那么客户端B也会加锁胜利。
这样的话,就导致了并发的问题,如果代码幂等性没有解决好,就会导致问题产生。
锁误删除
还是相似的问题,客户端A加锁同时设置超时工夫3秒,后果3s之后程序逻辑还没有执行实现,锁曾经开释。客户端B此时也来尝试加锁,这时客户端A代码执行实现,执行开释锁,后果开释了客户端B的锁。
那下面两个问题你有什么好的解决方案吗?
锁超时
这个有两个解决方案。
- 针对锁超时的问题,咱们能够依据平时业务执行工夫做大抵的评估,而后依据评估的工夫设置一个较为正当的超时工夫,这样能一大部分水平上防止问题。
- 主动续租,通过其余的线程为将要过期的锁缩短持有工夫
锁误删除
每个客户端的锁只能本人解锁,个别咱们能够在应用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节点之间齐全相互独立,彼此之间不存在主从同步和数据复制。
次要步骤如下:
- 获取以后Unix工夫
- 依照程序顺次尝试从多个节点锁,如果获取锁的工夫小于超时工夫,并且超过半数的节点获取胜利,那么加锁胜利。这样做的目标就是为了防止某些节点曾经宕机的状况下,客户端还在始终期待响应后果。举个例子,假如当初有5个节点,过期工夫=100ms,第一个节点获取锁破费10ms,第二个节点破费20ms,第三个节点破费30ms,那么最初锁的过期工夫就是100-(10+20+30),这样就是加锁胜利,反之如果最初工夫<0,那么加锁失败
- 如果加锁失败,那么要开释所有节点上的锁
那么RedLock有什么问题吗?
其实RedLock存在不少问题,所以当初其实个别不举荐应用这种形式,而是举荐应用Redission的计划,他的问题次要如下几点。
性能、资源
因为须要对多个节点别离加锁和解锁,而个别分布式锁的利用场景都是在高并发的状况下,所以耗时较长,对性能有肯定的影响。此外因为须要多个节点,应用的资源也比拟多,简略来说就是费钱。
节点解体重启
比方有1~5号五个节点,并且没有开启长久化,客户端A在1,2,3号节点加锁胜利,此时3号节点解体宕机后产生重启,就失落了加锁信息,客户端B在3,4,5号节点加锁胜利。
那么,两个客户端A\B同时获取到了同一个锁,问题产生了,怎么解决?
- Redis作者倡议的形式就是延时重启,比方3号节点宕机之后不要立即重启,而是期待一段时间后再重启,这个工夫必须大于锁的无效工夫,也就是锁生效后再重启,这种人为干涉的措施真正施行起来就比拟艰难了
- 第二个计划那么就是开启长久化,然而这样对性能又造成了影响。比方如果开启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。
解锁
- 如果key都不存在了,那么就间接返回
- 如果key、field不匹配,那么阐明不是本人的锁,不能开释,返回空
- 开释锁,重入次数-1,如果还大于0那么久刷新过期工夫,反之那么久删除锁
watchdog
也叫做看门狗,也就是解决了锁超时导致的问题,实际上就是一个后盾线程,默认每隔10秒主动缩短锁的过期工夫。
默认的工夫就是internalLockLeaseTime / 3
,internalLockLeaseTime
默认为30秒。
最初,理论生产中对于不同的场景该如何抉择?
首先,如果对于并发不高并且比较简单的场景,通过数据库乐观锁或者惟一主键的模式就能解决大部分的问题。
而后,对于Redis实现的分布式锁来说性能高,本人去实现的话比拟麻烦,要解决锁续租、lua脚本、可重入等一系列简单的问题。
对于单机模式而言,存在单点问题。
对于主从架构或者哨兵模式,故障转移会产生锁失落的问题,因而产生了红锁,然而红锁的问题也比拟多,并不举荐应用,举荐的应用形式是用Redission。
然而,不论抉择哪种形式,自身对于Redis来说不是强一致性的,某些极其场景下还是可能会存在问题。
对于Zookeeper的实现形式而言,自身就是保证数据一致性的,可靠性更高,所以不存在Redis的各种故障转移带来的问题,本人实现也比较简单,然而性能相比Redis稍差。
不过,理论中咱们当然是有啥用啥,老板说用什么就用什么,我才不论那么多。