分布式锁
2021-04-25 17:16:32 1 举报
AI智能生成
分布式锁总结
作者其他创作
大纲/内容
redis分布式锁
基本
直接setnx
直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴
缺点
如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到
setnx设置一个过期时间
为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放
缺点
setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题
set nx ex
扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:set key value ex 5 nx
缺点
如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁
上面所说的那个缺点还会导致当前的线程释放其他线程占有的锁
set nx ex 加一个事务id
上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务
第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚,其实也是变相地解决了第一个问题
get key和将value与id比较是两个步骤,不能保证原子性
set nx px + 事务id + lua
可以用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持
缺点
集群环境下,对master节点申请了分布式锁,由于redis的主从同步是异步进行的,master在内存中写入了nx之后直接返回,客户端获取锁成功,此时master节点挂了,并且数据还没来得及同步,另一个节点被升级为master,这样其他的线程依然可以获取锁
redlock
假设集群中所有的n个master节点完全独立,并且没有主从同步,此时对所有的节点都去setnx,并且设置一个请求过期时间re和锁的过期时间le,同时re必须小于le(可以理解,不然请求3秒才拿到锁,而锁的过期时间只有1秒),此时如果有n / 2 + 1个节点成功拿到锁,此次分布式锁就算申请成功
缺点
可靠性还没有被广泛验证,并且严重依赖时间,好的分布式系统应该是异步的,并不能以时间为担保,程序暂停、系统延迟等都可能会导致时间错误
缺陷
客户端长时间阻塞导致锁失效问题
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题
分支主题
启动另外一个线程去检查的问题,这个key是否超时,在某个时间还没释放的话如果业务没有处理完对比value值延长锁的时间
Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s
GC产生锁过期的依然处理不了
redis服务器时钟漂移问题
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题
系统时钟漂移原因
1.系统的时钟和NTP服务器不同步。这个目前没有特别好的解决方案,只能相信运维同学了。
2.clock realtime被人为修改。在实现分布式锁时,不要使用clock realtime。不过很可惜,redis使用的就是这个时间,Redis 5.0源码,使用的还是clock realtime。Antirez说过改成clock monotonic的,不过大佬还没有改。也就是说,人为修改redis服务器的时间,就能让redis出问题了
单点实例安全问题
如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁
redlock
获取当前时间戳(ms)
先设定key的有效时长(TTL),超出这个时间就会自动释放,然后client(客户端)尝试使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个比TTL短很多的超时时间,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例
client通过获取所有能获取的锁后的时间减去第一步的时间,还有redis服务器的时钟漂移误差,然后这个时间差要小于TTL时间并且成功设置锁的实例数>= N/2 + 1(N为Redis实例的数量),那么加锁成功
如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例
问题
如果有节点发生崩溃重启的话,还是有可能出现多个客户端同时获取锁的情况假设一共有5个Redis节点:A、B、C、D、E,客户端1和2分别加锁客户端1成功锁住了A,B,C,获取锁成功(但D和E没有锁住)节点C的master挂了,然后锁还没同步到slave,slave升级为master后丢失了客户端1加的锁客户端2这个时候获取锁,锁住了C,D,E,获取锁成功
RedLock并没有完全解决Redis单点故障存在的隐患,也没有解决时钟漂移以及客户端长时间阻塞而导致的锁超时失效存在的问题,锁的安全性隐患依然存在
zk分布式锁
实现
临时顺序节点
优点
zk通过临时节点,解决掉了死锁的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉,其他客户端自动获取锁zk通过节点排队监听的机制,也实现了阻塞的原理,其实就是个递归在那无限等待最小节点释放的过程可重入,创建临时节点时带上线程信息高可用的,只要半数以上的或者,就可以对外提供服务了
缺点
性能上可能并没有缓存服务那么高
每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能
ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上
网络抖动,客户端和ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了
zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。
RetryUntilElapsed(int maxElapsedTimeMs, int sleepMsBetweenRetries)以sleepMsBetweenRetries的间隔重连,直到超过maxElapsedTimeMs的时间设置
RetryNTimes(int n, int sleepMsBetweenRetries)指定重连次数
RetryOneTime(int sleepMsBetweenRetry)重连一次,简单粗暴
ExponentialBackoffRetryExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries)ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs)时间间隔和最大重试次数
zk server端timeout参数tickTime:zk的心跳间隔(heartbeat interval),也是session timeout基本单位.单位为毫秒.minSessionTimeout:最小超时时间,zk设置的默认值为2*tickTime.maxSessionTimeout:最大超时时间,zk设置的默认值为20*tickTime
Curator 的客户端超时设置.CuratorFrameworkFactory.newClient连接超时15sSession超时60s
总结
redis set px nx + 唯一id + lua脚本
优点
redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
缺点
依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响(CAP保证了AP),如果发生则需要人工介入
基于redis的redlock
优点
可以解决redis集群的同步可用性问题
不一定A、B、C、D、E,客户端2锁A,B,C然后C宕机,客户端2又能成功锁C,D,E,这样就需要master节点持久化开启,而且开启后性能下降
缺点
依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率锁删除失败 过期时间不好控制非阻塞,操作失败后,需要轮询,占用cpu资源
基于zk的分布式锁
优点
不存在redis的超时、数据同步(zookeeper是同步完以后才返回)、主从切换(zookeeper主从切换的过程中服务是不可用的 CAP满足的是CP)的问题,可靠性很高
缺点
依赖中间件,保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能不如redis
0 条评论
下一页