分布式锁🔒是个啥❓ 其实就这么点事
聊聊分布式锁,本文将以redis 为实现方式
Q:为什么会存在分布式锁
不同的进程需要用互斥的方式对共享的资源进行访问。
Q:加锁有什么好处
效率:加锁可以避免我们不必要的重复执行一个操作。比如说同一个计数器触发两次。
正确性:加锁可以防止数据被并发的修改,避免了数据的损坏、丢失、不一致之类的情况。
Q:实现分布锁可以使用什么
mysql
zookeeper
redis
Q:实现锁的时候需要注意什么
互斥:同一时间只能有一个进程持有锁。
容错:只要大多数redis节点启动,客户端就能获取和释放锁。
无死锁:即使加锁的客户端崩溃,其他客户端最终也是可以获取锁的。
Q:redis实现加锁🔐
redis加锁主要是通过
setnx
https://redis.io/commands/setnx 进行操作,setnx 是SET if Not eXists
的缩写SETNX : 如果 key 不存在,就通过key 来设置value保存字符串,操作返回结果
1
, 在这种情况下操作等同于set
。但是如果 key 存在时再进行SETNX
操作,value 不会改变,操作返回结果0
。
当操作需要加锁的时,通过 setnx
进行赋值,如果返回1
,说明加锁成功,执行完操作之后再进行del
操作,以便其他进程使用。
但是这样写是存在问题的,如果在 redisCli.setnx("hello","word")
设置时,客户端崩溃代码还没来得及设置过期时间,就会产生死锁。
所以需要改进加锁的代码,可以通过 set
https://redis.io/commands/set命令的第三个参数进行过期时间和不可更改的设置。
但是这样还是存在问题,如果进程a加锁成功,但是执行具体业务逻辑的代码超过了设置的过期时间,这时候锁过期失效,进程b就可以加锁成功。如果这时进程a执行完成也是可以删锁的,尽管现在锁属于进程b。所以需要对应每个进程加锁时进行区分,防止这种误删的操作出现。
可以通过设置一个唯一的uuid为value,保证删除时的准确性。
如果这么写的话,还是不完美的,在删除锁的时候是两步操作,不符合原子性。像这种操作可以根据实际需要决定是不是可以容忍的,毕竟就算是删除没有执行成功,锁还是会自动过期的。如果不能容忍的话,可以通过一段Lua代码根据key和value决定要不要删除,保证客户端调用的原子性。
关于加锁的失败的逻辑,你可以加锁失败后直接返回,也可以通过一个for 循环设置重试次数,当然也可以用一个while 循环直至加锁成功,具体实现可以根据业务逻辑自行判断。
Q:这样实现就是完美的了吗🤔️
以上的节点在单机的redis上是适用的,但是如果存在redis主从节点就会有问题。如果在集群中master节点由于某种原因发生了切换,可能会出现锁丢失的情况。试想这种情况:
master 节点已经加锁成功
基于redis的复制是异步的,这个锁有可能还没来得及同步到slave节点上
master发生故障挂了,故障转移,slave节点升级成为master
锁丢了,可能导致多个客户端可以加锁成功
面对这种情况,可以参考redis作者提出的更高级的实现方式redLock
https://redis.io/topics/distlock
Q:什么是 redLock
redLock 是一种算法,redis官方推荐的Java实现
Redisson
https://github.com/redisson/redisson中就实现了这种算法实现redLock 的前提是我们拥有N个master 节点,这些节点是相互独立的,互不干扰、独立运行
以同时拥有5个节点为例,为了获取锁,客户端需要进行:
获取当前的时间(毫秒为单位)
客户端将依次对5个节点使用相同的key、唯一的value以及附加参数 通过
set
尝试获取锁。当客户端向节点尝试获取锁时,应该设置一个网络响应和超时的时间,这个时间应该小于锁的失效时间。假如说你的锁的实效时间是10秒,那么网络响应和超时的设置应该在5-50毫秒之间。这样可以防止客户端长时间和处于故障的节点通信。如果某个节点不可用,应该尽快与下一个节点进行通信客户端获取当前时间,减去第1步的时间,就可以计算出获取锁时花费的时间,当且仅当大多数节点(N / 2 + 1,这里是3)获取到了锁,并且花费的时间小于锁的过期时间,锁才算获得成功。
如果客户端获取锁成功,那么锁的有效时间就是第1步的时间 减去 第2步流程的时间,就是第3步说的
如果客户端由于某种原因没有获取到锁(成功的节点少于大多数,或者获取有效时间为负),那客户端需要尝试解锁所有节点(即使有的节点根本没加锁成功)
对接redLock 的理解可以是:既然单节点不可靠,那我就多放几个节点,各个节点相互独立,没有从属关系,即使偶尔几个节点挂掉了,只要保证大多数节点能请求成功,那么加锁流程就没有问题。这样的5个节点,即使4和5 都挂了,只要123能请求成功,满足大多数条件,没有超时,加锁还是可以成功的。
Q:关于redLock 的爱恨纠葛⚔️
Martin Kleppmann(《设计数据密集型应用》的作者)曾经写文章https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html 分析了redLock的缺点,主要集中在这几点:
redLock 严重依赖服务器时间
redLock 没有保证锁的正确性
redis的作者 antirez 也写文章回击http://antirez.com/news/101,就几个问题提出了解决方案,想了解的同学可以自行阅读。
Q:简单聊聊Redisson
redLock的加锁、解锁也是通过Lua代码实现的,保证了原子性。
redisson 实现了一个
看门狗
机制https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8#84-%E7%BA%A2%E9%94%81redlock,主要是负责对锁进行监控,可以延长锁的有效期。可以用来避免由于Redisson客户端节点宕机或者或者其他原因造成的死锁情况。
Q:说在最后
redis 的分布式锁只看谁能加锁成功,如果不成功,要么进行重试,要么直接返回。如果希望所有的请求都以排队的形式进行等待,按照顺序一个一个处理,zookeeper无疑是最适合的。
版权声明: 本文为 InfoQ 作者【山中兰花草】的原创文章。
原文链接:【http://xie.infoq.cn/article/e9f83089e4c88c0558b01636c】。文章转载请联系作者。
评论