Redis 分布式锁原理及 Redisson 实现
Redis 分布式锁原理
Redis 分布式锁原理,可以直接看官方文档:
https://redis.io/commands/set#patterns
The command
SET resource-name anystring NX EX max-lock-time
is a simple way to implement a locking system with Redis.
SET resource-name anystring NX EX max-lock-time
命令可以基于 Redis 实现分布式锁。
NX
Only set the key if it does not already existEX seconds
Set the specified expire time, in seconds
-
NX
仅当 key 不存在时设置成功 -
EX seconds
失效时间(秒)
A client can acquire the lock if the above command returns
OK
(or retry after some time if the command returnsNil
), and remove the lock just usingDEL
.
- 当命令返回
OK
时,该客户端获得锁 - 当命令返回
Nil
时,客户端未获得锁,需要过一段时间再重试命令尝试获取锁 - 使用
DEL
删除命令可用来释放锁
The lock will be auto-released after the expire time is reached.
当达到失效时间时,锁自动释放。
It is possible to make this system more robust modifying the unlock schema as follows:
- Instead of setting a fixed string, set a non-guessable large random string, called token.
- Instead of releasing the lock with DEL, send a script that only removes the key if the value matches.
This avoids that a client will try to release the lock after the expire time deleting the key created by another client that acquired the lock later.
更加健壮的释放锁的方式:
- 设置的 value 是一个随机生成的无法预测的值,叫做 token
- 不再使用 DEL 直接删除 key 来释放锁,而是使用一个 script,仅当 value 匹配 token 时才会删除 key
这样可以防止某个客户端在超过失效时间后尝试释放锁,直接使用 DEL 可能会删除掉别的客户端添加的锁。
下面是释放锁脚本的例子:
1 | if redis.call("get",KEYS[1]) == ARGV[1] |
The script should be called with
EVAL ...script... 1 resource-name token-value
执行 EVAL ...script... 1 resource-name token-value
命令释放锁。
以上是官方文档中的内容,阅读到这里可以发现一个问题:
- 官方的方案中,分布式锁是有个失效时间的,达到失效时间锁会被自动释放,如果此时需要加锁执行的任务还未完成,同时锁又被其他客户端获取到,那么就可能会出现严重的问题;
- 如果锁不加上失效时间,万一获得锁的客户端突然 crash 了,没有来得及释放锁,那么这个锁就永远不会被释放。
针对这个问题,可以看下 Redisson 是如何解决的。
Redisson 分布式锁
官方文档:
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
通过以下方式,可以获得一个 key 为 myLock
的 RLock
对象:
1 | Config config = new Config(); |
获取锁和释放锁:
1 | lock.lock(); // 获取锁 |
RLock
提供了以下多种获取锁的方法:
-
void lock()
-
void lock(long leaseTime, TimeUnit unit)
-
void lockInterruptibly()
-
void lockInterruptibly(long leaseTime, TimeUnit unit)
-
boolean tryLock()
-
boolean tryLock(long time, TimeUnit unit)
-
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
RLock
实现了 java.util.concurrent.locks.Lock
接口,所以 RLock
是符合 Java 中的 Lock
接口规范的。以上的方法中,这四个方法是来源于 Java 中的 Lock
接口:
-
void lock()
获取锁,如果锁不可用,则当前线程一直等待,直到获得到锁 -
void lockInterruptibly()
和lock()
方法类似,区别是lockInterruptibly()
方法在等待的过程中可以被 interrupt 打断 -
boolean tryLock()
获取锁,不等待,立即返回一个 boolean 类型的值表示是否获取成功 -
boolean tryLock(long time, TimeUnit unit)
获取锁,如果锁不可用,则等待一段时间,等待的最长时间由long time
和TimeUnit unit
两个参数指定,如果超过时间未获得锁则返回 false,获取成功返回 true
除了以上四个方法外,还有三个方法不是来源于 Java 中的 Lock
接口,而是 RLock
中的方法。这三个方法和上面四个方法有一个最大的区别就是多了一个 long leaseTime
参数。 leaseTime
指的就是 Redis 中的 key 的失效时间。通过这三个方法获取到的锁,如果达到 leaseTime
锁还未释放,那么这个锁会自动失效。
回到上面的问题:如果设置了失效时间,当任务未完成且达到失效时间时,锁会被自动释放;如果不设置失效时间,突然 crash 了,锁又会永远得不到释放。Redisson 是怎么解决这个问题的呢?
If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be changed through
Config.lockWatchdogTimeout
setting.
为了防止 Redisson 实例 crash 导致锁永远不会被释放,针对未指定 leaseTime
的四个方法,Redisson 为锁维护了看门狗(watchdog)。看门狗每隔一段时间去延长一下锁的失效时间。锁的默认失效时间是 30 秒,可通过 Config.lockWatchdogTimeout
修改。延长失效时间的任务的执行频率也是由该配置项决定,是锁的失效时间的 1/3,即默认每隔 10 秒执行一次。
如果 Redisson 实例 crash 了,看门狗也会跟着 crash,那么达到失效时间这个 key 会被 Redis 自动清除,锁也就被释放了,不会出现锁永久被占用的情况。