使用Redis SETNX 命令实现分布式锁

使用Redis的 SETNX 命令可以实现分布式锁,本文介绍其实现方法。

直接进入正题,现在分布式的应用场景很多,为了保持数据的一致性,经常碰到需要对资源加锁的情形。 利用redis来实现分布式锁就是其中的一种实现方案。

SETNX命令简介
命令格式

SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值

设置成功,返回 1 。
设置失败,返回 0 。
示例

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 设置成功
(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                   # 没有被覆盖
"programmer"

SETNX分布式锁实现方案

利用SETNX的特性,很容易的想到,在需要加锁的时候,调用SETNX命令,如果返回了1,表示设置成功,获得了当前锁,之后做一些想要的操作,完成之后调用DEL命令释放锁。

redis> SETNX lock true    # 获得锁成功
(integer) 1
... do thing ...
redis> DEL lock    # 释放锁
(integer) 1

但是这样存在一个问题,如果在执行DEL命令之前,当前程序发生错误,那么这个锁就永远得不到释放,其他程序也永远无法加锁成功。

于是我们可以在加锁之后为这个锁设置一个过期时间,过期时间之后,如果没有释放,就自动删除,防止锁被一直占用。

redis> SETNX lock true    # 获得锁成功
(integer) 1
redis> EXPIRE lock 5    # 设置5秒的过期时间
(integer) 1
... do thing ...
redis> DEL lock    # 释放锁
(integer) 1


但是这样还是有问题,如果在SETNX和EXPIRE之间程序又发生了错误,当前锁又无法释放。所以根本原因还是需要一个原子的操作,在获得锁的同时能够同时设置锁的过期时间。

为了解决这个问题,Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行, 这个可以在下一篇介绍。 本文介绍另一种方式。

SETNX设置锁

在设置锁的时候,我们可以利用锁的值来实现过期的特性

SETNX lock  <current Unix time + lock timeout>

我们不是设置一个简单的值到lock中,而是将过期的时间写入到lock中。 获得锁的判断条件仍旧是跟之前一样, 如果返回了1的话,表示获得了锁,可以进行下一步的操作。

判断过期条件

正常情况下,操作完成之后,仍旧执行DEL操作将当前锁释放。那么如果当前程序发生了错误退出了,当前锁没有正常释放,其他的进程如何获得锁呢。

假设上一个进程加锁之后异常退出,没有释放锁。当前的进程想要加锁,在调用SETNX的时候发现加锁失败,然后需要调用GET命令获得当前锁的值,即上一个进程写入的过期时间。 如果获得的过期时间未到,那么当前进程继续等待; 如果锁的过期时间已经到了,很大的概率上一个获得锁的进程已经发生了错误,因为我们这个过期时间一般会设置的比正常的运行时间要长。在这种情况下, 当前进程可以重新写入这个锁并进行后续的操作。

解决竞争条件

但是这样又带来一个新的问题: 假设有P1和P2两个进程同时想获得锁,他们都检测到了当前的锁已经过期了, 他们可以写入,他们调用SET命令写入都会成功,那么如果决定到底是哪个进程获得了锁呢。

所以在这边重新写入的时候不能简单的调用SET命令, 还有另一个命令可以考虑: GETSET。GETSET命令在设置值的同时,会将设置之前的值返回。

仍旧考虑刚才的情形, P1和P2同时在竞争锁,发现锁的时间T已经过期了,然后他们同时调用GETSET命令设置新的锁。假设P1先设置成功时间T1,那么调用GETSET得到的值就是T; P2调用GETSET虽然将锁的时间设置成了T2,但是他得到的值是T1。

通过判断GETSET返回的值,就能判断自己是否获得了锁。如果返回的值仍然是一个过期的时间,那么说明正确的加锁了;否则的话,说明正好有别的进程已经设置了锁,当前进程只是更新了一下锁而已,就继续等待。

可能会说这边有一个小问题,P1设置的锁的过期时间被P2更改了。考虑到产生这种竞态条件的时候肯定时间间隔是非常小的, 即使重新设置了过期时间,这种很短的时间修改在大多数情况下都可以忽略不计。

伪代码

所以,我们能够得到最终的一个过程,用伪代码表示
 

while 1:
    lock = redis.SETNX(key, time.now() + timeout)
    if lock == 1:
        // 获得锁
        break
    lock_ts = redis.GET(key)
    if (lock_ts < time.now()) && (redis.GETSET(key, time.now() + timeout) < time.now()):
        // 锁已经过期,用GETSET重新写锁
        // 返回的原来的时间仍旧过期,说明加锁成功
        break
    else:
        sleep
        
.... do something ...

// 完成之后释放锁
redis.DEL(key)

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hekliu

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值