分布式锁--redis篇

一种方案

这个方案不用看了,直接看新方案,写出来只是想展示演变的过程

SETNX命令简介

对官方文档解释部分:

SETNX key value

将key的值设为value,并且仅当key不存在。
若给定的key已经存在,则SETNX不做任何操作。
SETNX 是SET if Not exists的简写。

返回整数,具体为
1,当 key 的值被设置
0,当 key 的值没被设置

使用SETNX实现分布式锁

多个进程执行以下Redis命令:

SETNX lock.foo <current Unix time + lock timeout + 1>

  • 如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
  • 如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。

解决死锁
正常第一反应利用SETNX实现分布式锁可能是这样的

if(SETNX key value){//如果设置成功表示拿到了锁
    return true;
}
return false;

然后释放锁的时候就直接 DEL掉;

简单思路是这样,但是这样会有很多问题:
如果一个进程获得锁之后,断开了与redis的连接(进程挂断或者网络中断),那么锁一直的不断释放,其他的进程就一直获取不到锁,就出现了 “死锁”
然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:

  1. P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
  2. P2和P3进程发现锁 lock.foo 已超时
  3. P2执行 DEL lock.foo命令
  4. P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
  5. P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
  6. P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
  7. P2和P3同时获得了锁

从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。

为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法:
我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:

进程P4执行 SETNX lock.foo 以尝试获取锁
由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败
P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测
如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁
假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。
另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。

代码:

LOCK_TIMEOUT = 3
lock = 0
lock_timeout = 0
lock_key = 'lock.foo'

# 获取锁
while lock != 1:
    now = int(time.time())
    lock_timeout = now + LOCK_TIMEOUT + 1
    lock = redis_client.setnx(lock_key, lock_timeout)
    if lock == 1 or (now > int(redis_client.get(lock_key))) and now > int(redis_client.getset(lock_key, lock_timeout)):
        break
    else:
        time.sleep(0.001)

# 已获得锁
do_job()

# 释放锁
now = int(time.time())
if now < lock_timeout:
    redis_client.delete(lock_key)

新的方案

在官网有一段文字:

Anyway even assuming a single-instance locking primitive, starting with 2.6.12 it is possible to create a much simpler locking primitive, equivalent to the one discussed here, using the SET command to acquire the lock, and a simple Lua script to release the lock. The pattern is documented in the SET command page.
无论如何,即使假设一个单独的实例锁定原语,从2.6.12开始,也可以创建一个更简单的锁定原语,相当于这里讨论的原语,使用SET命令获取锁,使用一个简单的Lua脚本释放锁。该模式记录在SET命令页中。

意思就是有一个set就可以实现了,不用setnx了。

set 添加参数 和lua脚本配合

SET key value [EX seconds|PX milliseconds] [NX|XX]
Starting with Redis 2.6.12 SET supports a set of options that modify its behavior:
EX seconds – Set the specified expire time, in seconds.
PX milliseconds – Set the specified expire time, in milliseconds.
NX – Only set the key if it does not already exist.
XX – Only set the key if it already exist.

官方鼓励的模式:
上锁:

SET resource-name anystring NX EX max-lock-time

如果返回ok,设置锁成功
如果返回Nil,隔断时间再试。

解锁的时候,用del
时间期限到了以后,会自动解锁。

让系统更健壮的方案如下:

  1. 不要设置固定字符串,而是设置一个不可猜测的大随机字符串,称为token。
  2. 不使用DEL释放锁,而是发送一个脚本,该脚本只在值匹配时移除密钥。这样可以避免客户端在过期后尝试释放锁,删除由另一个稍后获取锁的客户端创建的密钥。
    脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
The script should be called with EVAL ...script... 1 resource-name token-value

关于redis这个方案有图讲的比较好的一篇文章,关于redis分布式锁

redlock 协议

关于redlock官方文档 点这里走起

在使用nodejs 实现的redlock模块的过程中发现的问题:
如果在调用lock的时候,传递的多个redisclient其中一个关闭以后,lock会卡住,一直等待redis的回复。后续继续研究这个问题…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值