一、redis分布式锁的基本实现
redis加锁命令:redis
SETNX resource_name my_random_value PX 30000
这个命令的做用是在只有这个key不存在的时候才会设置这个key的值(NX选项的做用),超时时间设为30000毫秒(PX选项的做用) 这个key的值设为“my_random_value”。这个值必须在全部获取锁请求的客户端里保持惟一。算法
SETNX 值保持惟一的是为了确保安全的释放锁,避免误删其余客户端获得的锁。举个例子,一个客户端拿到了锁,被某个操做阻塞了很长时间,过了超时时间后自动释放了这个锁,而后这个客户端以后又尝试删除这个其实已经被其余客户端拿到的锁。因此单纯的用DEL指令有可能形成一个客户端删除了其余客户端的锁,经过校验这个值保证每一个客户端都用一个随机字符串’签名’了,这样每一个锁就只能被得到锁的客户端删除了。安全
既然释放锁时既须要校验这个值又须要删除锁,那么就须要保证原子性,redis支持原子地执行一个lua脚本,因此咱们经过lua脚本实现原子操做。代码以下:服务器
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
二、业务逻辑执行时间超出锁的超时限制致使两个客户端同时持有锁的问题
若是在加锁和释放锁之间的逻辑执行得太长,以致于超出了锁的超时限制,就会出现问题。由于这时候第一个线程持有的锁过时了,临界区的逻辑尚未执行完,这个时候第二个线程就提早从新持有了这把锁,致使临界区代码不能获得严格的串行执行。并发
不难发现正常状况下锁操做完后都会被手动释放,常见的解决方案是调大锁的超时时间,以后若再出现超时带来的并发问题,人工介入修正数据。这也不是一个完美的方案,由于但业务逻辑执行时间是不可控的,因此仍是可能出现超时,当前线程的逻辑没有执行完,其它线程乘虚而入。而且若是锁超时时间设置过长,当持有锁的客户端宕机,释放锁就得依靠redis的超时时间,这将致使业务在一个超时时间周期内不可用。dom
基本上,若是在执行计算期间发现锁快要超时了,客户端能够给redis服务实例发送一个Lua脚本让redis服务端延长锁的时间,只要这个锁的key还存在并且值还等于客户端设置的那个值。 客户端应当只有在失效时间内没法延长锁时再去从新获取锁(基本上这个和获取锁的算法是差很少的)。分布式
启动另一个线程去检查的问题,这个key是否超时,在某个时间还没释放。lua
当锁超时时间快到期且逻辑未执行完,延长锁超时时间的伪代码:spa
if redis.call("get",KEYS[1]) == ARGV[1] then
redis.call("set",KEYS[1],ex=3000)
else
getDLock();//从新获取锁
三、redis的单点故障主从切换带来的两个客户端同时持有锁的问题
生产中redis通常是主从模式,主节点挂掉时,从节点会取而代之,客户端上却并无明显感知。原先第一个客户端在主节点中申请成功了一把锁,可是这把锁尚未来得及同步到从节点,主节点忽然挂掉了。而后从节点变成了主节点,这个新的节点内部没有这个锁,因此当另外一个客户端过来请求加锁时,当即就批准了。这样就会致使系统中一样一把锁被两个客户端同时持有,不安全性由此产生。线程
不过这种不安全也仅仅是在主从发生 failover 的状况下才会产生,并且持续时间极短,业务系统多数状况下能够容忍。
四、RedLock算法
若是你很在意高可用性,但愿挂了一台 redis 彻底不受影响,能够考虑 redlock。 Redlock 算法是由Antirez 发明的,它的流程比较复杂,不过已经有了不少开源的 library 作了良好的封装,用户能够拿来即用,好比 redlock-py。
import redlock
addrs = [{
"host": "localhost",
"port": 6379,
"db": 0
}, {
"host": "localhost",
"port": 6479,
"db": 0
}, {
"host": "localhost",
"port": 6579,
"db": 0
}]
dlm = redlock.Redlock(addrs)
success = dlm.lock("user-lck-laoqian", 5000)
if success:
print 'lock success'
dlm.unlock('user-lck-laoqian')
else:
print 'lock failed'
RedLock算法的核心原理:
使用N个彻底独立、没有主从关系的Redis master节点以保证他们大多数状况下都不会同时宕机,N通常为奇数。一个客户端须要作以下操做来获取锁:
1.获取当前时间(单位是毫秒)。
2.轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每一个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。好比若是锁自动释放时间是10秒钟,那每一个节点锁请求的超时时间多是5-50毫秒的范围,这个能够防止一个客户端在某个宕掉的master节点上阻塞过长时间,若是一个master节点不可用了,咱们应该尽快尝试下一个master节点。
3.客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁((N/2) +1),并且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4.若是锁获取成功了,那如今锁自动释放时间就是最初的锁释放时间减去以前获取锁所消耗的时间。
5.若是锁获取失败了,不论是由于获取成功的锁不超过一半(N/2+1)仍是由于总消耗时间超过了锁释放时间,客户端都会到每一个master节点上释放锁,即使是那些他认为没有获取成功的锁。
五、知识扩展
5.1为何lua脚本结合redis命令能够实现原子性
Redis 提供了很是丰富的指令集,可是用户依然不知足,但愿能够自定义扩充若干指令来完成一些特定领域的问题。Redis 为这样的用户场景提供了 lua 脚本支持,用户能够向服务器发送 lua 脚原本执行自定义动做,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程当中不会被任意其它请求打断。
5.2 redis 可重入分布式锁
要实现可重入锁,方法很简单,当加锁失败时判断锁的值是否是跟当前线程设置值相同,伪代码以下:
if setnx == 0
if get(key) == my_random_value
//重入
else
//不可重入
else