本文重点并不在于提供一个可运行的Redis分布式锁示例,而是结合图文理解redis的分布式锁实现上的细节,以及为何要这样作。redis
用伪代码的形式简单介绍实现方式安全
SET resource_name my_random_value NX EX 30
复制代码
经过lua脚本实现原子的 比较 & 删除bash
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
复制代码
let randStr = Math.random();
let success = redis.set(key, randStr, 'EX', 30, 'NX');
if(success){
// 获取到锁
doSomething(); //执行要作的任务
//执行完释放锁
redis.call(` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`,key,randStr);
}else{
//未获取到锁
}
复制代码
这样看起来,Redis实现分布式锁用起来很简单嘛。服务器
可是,为啥这样写?dom
先回答后一个问题,释放锁以前为何要先判断值相等呢,为何不直接一句del key
多方便啊。分布式
首先要知道,分布式锁一个最基本的功能就是实现互斥,同一个时刻,应当只有一个客户端可以持有锁。ui
若是直接del key
,则可能会出现下面的状况lua
del key
将B当前正在持有的锁释放了 (但B此时不知情,还觉得本身持有锁,继续执行本身的任务)(激活生命线表示有客户端持有锁,按颜色对应)spa
再来看看使用随机值是怎么避免这一问题的。code
首先,客户端获取锁的时候,设置的value为各个客户端本身生成的随机字符串。
在步骤5,因为客户端A和B的随机值不同,只会走lua脚本里的else分支,而不进行删除,这样客户端C天然也就没有了可乘之机。
总的来讲,随机字符串是用做每一个客户端的“标识”,来确保客户端只能删除本身获取的锁,而不会误删其余客户端的。
使用了随机字符串后的时序图
看到这,你也许会想:不对呀,客户端B和C存在重叠的部分(图1中红蓝),表示同时持有锁了。但AB也有重叠部分啊(图1中蓝绿),这是否是有bug?
对不起,这还真不算bug,只能算是 feature。
由于要防止客户端崩溃而致使锁永不释放,锁的过时时间是不得不加的。
因此,这里只能根据获取锁后要执行的任务内容,来设置一个合适的锁过时时间。
或者采用部分语言中客户端的方式,在过时前延长过时时间。
上面的例子中释放锁时用了这么一个lua脚本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
复制代码
为何不直接在代码里写呢?
let value = redis.get(key);
if(randStr == value){
redis.del(key);
}
复制代码
很少废话,直接上图
图中能够看出,若是不使用lua脚本将“比较&删除”这两步合在一块儿造成原子操做,那么读取和删除这两个步骤之间可能会出现原有锁过时,又被另外一个客户端B获取的状况。
而原客户端A不知情,仍然执行了删除,删掉了客户端B正常获取的锁,最终致使BC同时持有,不知足互斥性。
单个redis节点实现的分布式锁虽然通常来讲够用了,但记住它并不是彻底可靠。
前面的场景中都假设了Redis服务可以正常运行,但若是发生了主节点的切换,由从节点顶上,仍可能致使多客户端都认为本身持有锁的状况。
这个场景比较简单:
因此若是须要避免单个redis节点崩溃切换后丢数据的问题,实现更高级别的保障,能够用多个redis节点实现的 redlock
注意:即便是redlock,仍不能100%保证可靠,只是比单个redis更安全一些。