Redis实现分布式锁——set命令+lua脚本实现
在分布式锁的实现中使用CAS的方式,比如 Redis 提供了一个保证原子性的 setnx 函数,多个线程调用该函数操作同一个 key 的时候,只有一个线程会返回 OK,其他线程返回 null,那么多个 JVM 中的线程同时设置同一个 key 时候只有一个 JVM 里面的一个线程可以返回 OK,返回 OK 的线程就相当于获取了全局锁,返回 null 的线程则可以选择自旋重试。获取到锁的线程使用完毕后调用 del 函数删除对应的 key,然后自旋的线程就会有一个返回 OK。
由于需要保证只有获取锁的线程才能释放锁,所以需要在获取锁时候调用 set 方法传递一个唯一的 value 值,可以传递请求 id;然后在释放锁的时候需要调用 get 方法获取 key 对应的 value,如果 value 值等于当前线程的请求 id 则说明是当前线程获取的锁,则调用 del 方法删除该 key 对应的 value,这就相当于当前线程释放了锁;如果 value 不等于当前线程的请求 id 则不做删除操作。
可见释放锁的操作需要调用 get 方法,然后 if 语句进行判断,判断 OK 然后调用 del 删除,而这三步并不是原子性的。Redis 有一个叫做 eval 的函数,支持 Lua 脚本执行,并且能够保证脚本执行的原子性,也就是在执行脚本期间,其它执行 Redis 命令的线程都会被阻塞。
- 通过 tryLock 方法尝试获取锁,内部是具体调用 Redis 的 set 方法,多个线程同时调用 tryLock 时候, 会同时调用 set 方法,但是 set 方法本身是保证原子性的,对应同一个 key 来说,多个线程调用 set 方法时候只有一个线程返回 OK,其它线程因为 key 已经存在会返回 null,返回 OK 的线程就相当与获取到了锁,其它返回 null 的线程则相当于获取锁失败
- 通过 lock 方法让使用 tryLock 获取锁失败的线程本地自旋转重试获取锁,这类似 JUC 里面的 CAS
- 通过 unLock 方法使用 redis 的 eval 函数传递 lua 脚本来保证操作的原子性
红锁 RedLock
基于Redis实现分布式锁存在一定的缺陷,它加锁只作用在一个Redis节点上,如果Master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
1、客户端1在Redis的Master节点上拿到了锁
2、Master宕机了,存储锁的key还没有来得及同步到Slave上
3、Master故障,发生故障转移,slave节点升级为Master节点
4、客户端2从新的Master获取到了对应同一个资源的锁
于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破了。针对这个问题,Redis社区提出了RedLock算法来解决这个问题。RedLock算法实现思路,客户端按照下面的步骤来获取锁:
1、获取当前时间的毫秒数T1
2、按顺序依次向N个Redis节点执行获取锁的操作,包括唯一UUID作为Value以及锁的过期时间。为了保证在某个在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还需要一个超时时间,它应该远小于锁的过期时间。客户端向某个Redis节点获取锁失败后,应立即尝试下一个Redis节点。这里失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有。
3、计算整个获取锁过程的总耗时,即当前时间减去第一步记录的时间。如果客户端从大多数Redis节点(>N/2 +1)成功获取到锁,并且获取锁总共消耗的时间小于锁的过期时间,则认为客户端获取锁成功,否则,认为获取锁失败
4、如果获取锁成功,需要重新计算锁的过期时间,它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间
5、如果最终获取锁失败,那么客户端立即向所有Redis系欸但发起释放锁的操作
延迟重启(delayed restarts)
但是,这还是不能解决故障重启后带来的安全性问题,可能出现了同一把锁同时被客户端1和客户端2同时持有。为何持久化策略无法避免重启的数据丢失?在redis中,如果是采用aof默认情况下是每秒写一次磁盘,即fsync操作,因此最坏的情况下可能会丢失一秒的数据。
为了解决这一问题,Redis 又提出了延迟重启(delayed restarts)的概念。当一个节点挂了,不要立即重启它,而是要等待一定的时间再重启,而且等待时间应该大于锁的过期时间(TTL)。这样的目的是保证这个节点在重启之前参与过的所有锁都已经过期了。
但是有个问题:在等待期间,该节点是不对外工作的,所以如果大多数的节点都挂掉了,进入了等待,就会导致了系统不可用,因为在此期间,任何锁都没有办法成功被加锁
604

被折叠的 条评论
为什么被折叠?



