Redis setNX 实现分布式锁(重复数据插入可用其来实现排他锁)
2016-12-06 15:53
423 查看
使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法。
SETNX key value
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
- 1,当 key 的值被设置
- 0,当 key 的值没被设置
redis> SETNX mykey “hello”
(integer) 1
redis> SETNX mykey “hello”
(integer) 0
redis> GET mykey
“hello”
redis>
如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。
上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。
然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:
P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
P2和P3进程发现锁 lock.foo 已超时
P2执行 DEL lock.foo命令
P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
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 的旧值是否小于当前时间,可以判断进程是否已获得锁
假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。
另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。
public static boolean acquireLock(String lock) {
// 1. 通过SETNX试图获取一个lock
boolean success = false;
Jedis jedis = pool.getResource();
long value = System.currentTimeMillis() + expired + 1;
System.out.println(value);
long acquired = jedis.setnx(lock, String.valueOf(value));
//SETNX成功,则成功获取一个锁
if (acquired == 1)
success = true;
//SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时
else {
long oldValue = Long.valueOf(jedis.get(lock));
//超时
if (oldValue < System.currentTimeMillis()) {
String getValue = jedis.getSet(lock, String.valueOf(value));
// 获取锁成功
if (Long.valueOf(getValue) == oldValue)
success = true;
// 已被其他进程捷足先登了
else
success = false;
}
//未超时,则直接返回失败
else
success = false;
}
pool.returnResource(jedis);
return success;
}
SETNX命令简介
命令格式
SETNX key value
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
返回值
返回整数,具体为- 1,当 key 的值被设置
- 0,当 key 的值没被设置
例子
redis> SETNX mykey “hello”
(integer) 1
redis> SETNX mykey “hello”
(integer) 0
redis> GET mykey
“hello”
redis>
使用SETNX实现分布式锁
多个进程执行以下Redis命令:SETNX lock.foo <current Unix time + lock timeout + 1>
如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。
解决死锁
考虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。
然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:
P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
P2和P3进程发现锁 lock.foo 已超时
P2执行 DEL lock.foo命令
P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
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 操作会导致把其他进程已获得的锁释放掉。
程序代码
用以下python代码来实现上述的使用 SETNX 命令作分布式锁的算法。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) java代码:
public static boolean acquireLock(String lock) {
// 1. 通过SETNX试图获取一个lock
boolean success = false;
Jedis jedis = pool.getResource();
long value = System.currentTimeMillis() + expired + 1;
System.out.println(value);
long acquired = jedis.setnx(lock, String.valueOf(value));
//SETNX成功,则成功获取一个锁
if (acquired == 1)
success = true;
//SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时
else {
long oldValue = Long.valueOf(jedis.get(lock));
//超时
if (oldValue < System.currentTimeMillis()) {
String getValue = jedis.getSet(lock, String.valueOf(value));
// 获取锁成功
if (Long.valueOf(getValue) == oldValue)
success = true;
// 已被其他进程捷足先登了
else
success = false;
}
//未超时,则直接返回失败
else
success = false;
}
pool.returnResource(jedis);
return success;
}
相关文章推荐
- mysql用语句实现重复数据不插入数据
- 可以插入重复数据的AVL树模板实现
- mysql insert插入时实现如果数据表中主键重复则更新,没有重复则插入的四种方法
- mysql insert插入时实现如果数据表中主键重复则更新,没有重复则插入的四种方法
- Vue.js在数组中插入重复数据的实现代码
- MySQL使用UNIQUE实现数据不重复插入
- mysql 使用 insert ignore into和unique实现不插入重复数据功能
- arcengine,深入理解游标Cursors,实现数据的快速查找,插入,删除,更新
- PHP+MySQL实现插入数据功能
- 用SQL Server数据库实现批量插入数据
- 我写的:用游标实现:查找重复数据并将重复数据加自动增长数字
- vc++ 使用command 对象通过parameter 实现插入二进制数据
- 如何实现插入并更新数据
- jsp或struts如何避免Form重复提交,不然数据中的插入的纪录有重复的!
- asp下实现批量插入数据的方法
- asp实现批量插入表单中的数据到数据库的方法
- jsp或struts如何避免Form重复提交,不然数据中的插入的纪录有重复的!
- asp实现批量插入表单中的数据到数据库的方法
- postgresql函数存储过程实现数据批量插入
- 用HashSet()实现重新建立一个没有重复数据的数组