转载 Redlock(redis分布式锁)原理分析
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);
官网文档地址如下:https://redis.io/topics/distlock
这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法;
因为redis在进行主从复制时是异步完成的,比如,在clientA获取完锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制从redis中,然后从redis选举一个升级为主redis,造成新的主redis没有clientA设置的锁,这时clientB尝试获取锁,并且能够成功获取锁,导致互斥失效。
===》 失败的原因:从redis立刻升级为主redis,如果能够过TTL时间再升级为主redis(延迟升级),或者立刻升级为主redis但是经过TTL的时间再执行获取任务,就能产生互斥效果,就能实现基于redis的主从redlock。
1、设置锁时,使用set命令。因为包含了setnx,expire的功能,起到原子操作效果,给key设置随机值,并且只有key不存在才设置成功返回true,并且设置key的过期时间。(最好用毫秒)
从2.6.12版本开始,redis为SET命令增加了一系列选项(set [key] NX/XX EX/PX [expiration]):
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值
原文地址:https://redis.io/commands/set
中文地址:http://redis.cn/commands/set.html
SET key_name my_random_value NX PX 30000
# NX 表示if not exist 就设置并返回True,否则不设置并返回False
# PX 表示过期时间用毫秒级, 30000 表示这些毫秒时间后此key过期
2、在获取锁后,并完成相关业务操作,需要删除自己设置的锁(必须是只能删除自己设置的,不能删除别人设置的锁)。
删除原因:保证服务器资源的高利用效率,不用等到锁自动过期才删除;
删除方法:最好使用Lua脚本删除(redis保证执行此脚本时不执行其他操作,保证操作的原子性),
代码如下;逻辑是 先获取key,如果存在并且值是自己设置的就删除此key;否则就跳过;
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
算法流程图如下:
假设有5个完全独立的redis主服务器
1.获取当前时间戳
2、client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的,获取时间,比锁的过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务,并且试着获取下一个redis实例。比如,TTL为5s,设置获取锁的时间最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下一个锁。
3、client通过获取所有能够获取的锁后的时间,减去第一步时间,这个时间差要小于TTL并且至少有3个redis实例获取成功,才算真正的获取锁成功。
4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁.
可以看成异步:因为即使进程之间(多个电脑间)没有同步时钟,但是每个进程时间流速大致相同,
并且时钟漂移相对TTL很小,可以忽略,所以可以看出是同步(不够严谨,算法上要算上时钟漂移,
因为如果两台电脑分别在地球的两端,则漂移时间很大)
当client不能获取锁时,应该在随机时间后重试获取锁。并且最好在同一时刻并发的把set命令
发送给所有的redis实例,而且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间。
由于释放锁会判断这个锁的value是不是自己设置的,如果是才删除,所以在释放锁时非常简单,
只要向所有实例发出释放锁的命令,不用考虑能否释放成功。
1、先假设client获取所有实例,所有实例包含相同的key和过期时间TTL。但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移。
2、对于N/2+1(也就是一半以上)的方式判断获取锁成功,是因为小于一半判断成功的话,有可能出现多个client都成功获取锁的情况,从而导致互斥锁失效。
3、一个client锁定大多数实例耗费的时间大于或者接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务);只要在TTL时间内成功获取一半以上的锁便是有效的,否则无效。
1、能够自动释放锁。
2、在获取锁失败(不到一半以上),或者任务完成后,能够自动释放锁,不用等到其自动过期。
3、在client重试获取锁之前,(第一次失败到第二次重试间隔时间)大于第一次获取锁消耗的时间。
4、重试获取锁要有一定次数限制。
1.如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
2.如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;
所以在锁完全有效性和性能方面要有所取舍;
3.有效解决既保证锁完全有效性及性能高效及即使断电情况的方法是redis同步到磁盘方式保持默认的每秒,在redis无论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是 在TTL时间内服务相当于暂停状态。
1、TTL时长 大于 正常业务执行时间 + 获取所有redis锁消耗时间 + 时钟漂移。=
2、获取redis所有服务消耗的时间要远小于 TTL时间,尝试获取每个redis实例锁的时间要 远小于 TTL时间。
并且获取成功的锁个数要在总数的一半以上(N/2+1)
3、尝试获取所有锁失败后 重新尝试一定要有一定次数限制。
4、在redis崩溃后(无论一个还是所有),要延迟TTL时间重启redis
5、在实现多redis节点时要结合单节点分布式锁算法 共同实现
推荐 基于redis的分布式锁的分析与实践
https://my.oschina.net/wnjustdoit/blog/1606215