逻辑门 处理器 寄存器 内存 CPU 指令集
主页 正文

Redis分布式锁实现与优化详解

如何用redis实现分布式锁

Redis 有一系列以 NX 结尾的命令。
NX 是 NoteXists 的缩写。
例如,SETNX命令应理解为SETifNoteXists。
这组命令非常有用。
本文介绍如何使用 SETNX 实现分布式锁定。
使用 SETNX 实现分布式锁定 使用 SETNX 实现分布式锁定非常简单。
示例:客户端想要获取名为 foo 的锁。
客户端使用以下命令获取: 如果 SETNXlock.foo 返回 1,则客户端获取锁并更改锁的键值。
如果将 .foo 键设置为指示其被锁定的时间值,则客户端最终可以通过 DELlock.foo 将其解锁。
返回 0 意味着另一个客户端已经获取了锁。
此时,要么先返回,重试,等待对方说完,要么等待锁超时。
上述解决死锁的加锁逻辑存在问题。
如果持有锁的客户端失败或崩溃而无法释放锁,该如何解决? 您可以通过查看锁定密钥对应的时间戳来确定是否发生了这种情况。
如果当前时间大于lock.foo的值,则说明锁已过期,可以重新使用。
如果发生这种情况,您不能简单地通过 DEL 清除锁定,然后再次运行 SETNX。
如果多个客户端检测到锁已超时,则此处可能会发生竞争条件。
操作 C0 超时,但仍持有锁的 C1 和 C2 读取 lock.foo,检查时间戳,发现超时。
C1 发送 DELlock.foo。
C1 发送 SETNXlock.foo 并成功。
C2 发送 DELlock.foo。
C2 发送 SETNXlock.foo 并成功。
这样C1和C2都获得了锁! 大问题! 幸运的是,这个问题是可以避免的。
客户端C3发送SETNXlock.foo来获取锁。
由于C0仍然持有锁,Redis返回0给C3,C3发送foo进行确认。
锁是否超时,如果没有,则等待或重试。
相反,如果发生超时,C3会尝试通过以下动作获取锁。
GETSETlock.foo 如果C3通过GETSET获取的时间戳仍然超时,则说明C3已经获取到锁。
如预期锁定。
如果在C3之前调用C4的客户端比C3快一步执行上述操作,则C3获得的时间戳就是没有按预期获取锁的值,因此它必须等待。
请再试一次。
虽然C3未能获取锁并重写了C4设置的锁超时值,但是这个非常小的错误的影响可以忽略不计。
注意:为了使分布式锁算法更加可靠,持有锁的客户端在释放锁之前必须仔细检查其锁是否超时,然后执行 DEL 操作。
这是因为客户端可能会由于耗时的过程而被阻止。
操作已暂停。
一旦操作完成,现在就不需要释放锁了,因为由于超时其他人已经获取了它。
示例伪代码 基于上面的代码,我编写了一个小的伪代码片段来说明使用分布式锁定的整个过程。
#getlocklock=0whilelock!=1:timestamp=currentUnixtime+locktimeout+1lock=SETNXlock.footimestampiflock==1or(now ( )>(GETlock.foo)and no w()>(GETSETlock.footimestamp)):break;else:sleep(10ms)#doyourjobdo_job()#releaseifnow()
立刻就想到了,但是当你使用Java时,是否也会想到那个人? AOP+注解? 伟大的。
使用起来很舒服。
不要重复代码。

大家所推崇的 Redis 分布式锁,真的可以万无一失吗?

使用Redis实现分布式锁最简单的解决方案是使用SETNX命令。
SETNX(SETifNoteXist)的使用方法是:SETNXkeyvalue。
只有当键key不存在时,才会将键值设置为值。
如果密钥存在,SETNX 不采取任何操作。
设置成功时返回SETNX,设置失败时返回0。
当想要获取锁时,直接使用SETNX来获取锁。
当想要释放锁时,使用DEL命令删除对应的键。
上述方案有一个致命的问题,即如果线程获得锁后由于某些异常因素(如宕机)而无法正常执行锁解锁操作,则该锁永远不会被释放。
为此,我们可以为该锁添加超时。
我们第一次想到Redis的EXPIRE命令(EXPIREkeythans)。
但这里我们不能使用EXPIRE来实现分布式锁,因为它和SETNX是两个操作,这两个操作之间可能会出现异常,所以还是没有达到预期的结果

对此,方法应该是正确的,是使用命令“SETkeyvalue[EX Seconds] [PXmilli Seconds] [NX|XX]”。
从 Redis 版本 2.6.12 开始,可以通过一系列参数修改 SET 命令的行为: EX Seconds:设置密钥过期时间(以秒为单位)。
执行 SETkeyvalueEX Seconds 的效果与执行 SETEXkeyvaluesvalues 的效果相同。
PXmillithans:设置密钥过期时间为毫秒。
执行 SETkeyvaluePXmillithans 的效果相当于执行 PSETEXkeymillithansvalue。
NX:仅当该键不存在时才设置该键。
执行 SETkeyvalueNX 的效果与执行 SETNXkeyvalue 等效。
XX:仅当密钥已存在时才设置该密钥。

以毫秒为单位执行 SETkeyvaluePX 的影响相当于执行 PSETEXkeymillithansvalue。
NX:仅当该键不存在时才设置该键。
执行SETkeyvalueNX的效果与执行SETNXkeyvalue的效果相同。
XX:仅当密钥已存在时才设置该密钥。
比如我们需要创建一个分布式锁,并将过期时间设置为10秒,那么我们可以执行如下命令:

这里我们一眼就能看出问题所在:GET和DEL是两个独立的操作,在GET执行和DEL执行之前的间隙可能会出现异常。
如果我们只要确保解锁代码是原子的,问题就迎刃而解了。
这里我们介绍一种新的方法,Lua脚本。
解锁的时候我们还是使用DEL命令来解锁。
修改后的方案看似理想,但现实中仍存在问题。
想象一下,某个线程获取了锁并设置了过期时间为10秒,然后进行了一次逻辑执行工作15秒。
此时,线程A获得的锁已经通过Redis的过期机制自动释放了。
线程A获得锁后,经过10秒,变化的锁可能已经被其他线程获取了。
当线程A执行完业务逻辑并准备解锁(DELkey)时,它可能会删除其他线程获取的锁。
总的来说,Redis分布式锁并不是万无一失的。

热门资讯
全球最大时钟历史与科技的完美融合
计算FPGA最大时钟频率的实用方法
社区安全与家庭和谐事迹精选
Makefile变量定义与引用指南
8086汇编语言加减乘除编程入门教程
工频电路电阻电感电容串联电压计算与波形解析
总线协议分为哪四类
手机缓存清理攻略告别卡顿,优化运行效率