参考:How to do distributed locking
参考:Redis分布式锁-RedLock算法
1.分布式锁
当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
2.分布式锁实现
1.加锁
通过setnx命令实现加锁操作,并设置过期时间
SETNX resource_name my_random_value PX 30000
- 设置过期时间
是为了预防死锁的可能性,当业务处理超过这个过期时间并且没有自动延长时间,则视为该锁自动释放 - my_random_value(SETNX的值)
这个值必须在所有获取锁请求的客户端里保持唯一
保持唯一的原因是:
SETNX 值保持唯一的是为了确保安全的释放锁,避免误删其他客户端得到的锁
如果一个客户端拿到了锁,被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁,然后这个客户端之后又尝试删除这个其实已经被其他客户端拿到的锁。所以单纯的用DEL指令有可能造成一个客户端删除了其他客户端的锁,通过校验这个值保证每个客户端都用一个随机字符串’签名’了,这样每个锁就只能被获得锁的客户端删除了。
redis支持原子地执行一个lua脚本,所以我们通过lua脚本实现原子操作。
2.当执行时间超出锁的超时限制
问题描述
如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。
解决方案
如果在执行计算期间发现锁快要超时了,客户端可以给redis服务实例发送一个Lua脚本让redis服务端延长锁的时间,只要这个锁的key还存在而且值还等于客户端设置的那个值。 客户端应当只有在失效时间内无法延长锁时再去重新获取锁(基本上这个和获取锁的算法是差不多的)。
3.单点故障主从切换带来的两个客户端同时持有锁的问题
问题描述
生产中redis一般是主从模式,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。
解决方案
使用RedLock算法
4.RedLock算法
算法核心
使用N个完全独立、没有主从关系的Redis master节点以保证他们大多数情况下都不会同时宕机,N一般为奇数。
-
获得当前时间(ms)
-
首先设置一个锁有效时间valid_time,也就是超过这个时间后锁自动释放,使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个小于valid_time的超时时间,比如valid_time时10s,那超时时间可以设置成50ms,如果这个实例不行,那么换下一个设置
-
计算获取锁总共占用的时间,再加上时钟偏移,如果这个总时间小于valid_time,并且成功设置锁的实例数>= N/2 + 1,那么加锁成功
RedLock是安全的吗
参考:How to do distributed locking
上面这个文章介绍了时钟前跳、GC引起的超时的情况导致RedLock不可靠的情况
这边说的 stop-th-world GC,在我们的JVM专栏里面会讲到,请参考.
正常情况下如果使用分布式锁是为了保证数据的准确性,应该选择zk来实现,因为zkid递增,可以对小于当前zxid的失效服务端的提交进行拒接。