Redis 分布式锁原理及 Redisson 实现

Redis 分布式锁原理

Redis 分布式锁原理,能够直接看官方文档:
https://redis.io/commands/set...java

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 实现分布式锁。git

  • NX Only set the key if it does not already exist
  • EX 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 returns Nil), and remove the lock just using DEL.
  • 当命令返回 OK 时,该客户端得到锁
  • 当命令返回 Nil 时,客户端未得到锁,须要过一段时间再重试命令尝试获取锁
  • 使用 DEL 删除命令可用来释放锁
The lock will be auto-released after the expire time is reached.

当达到失效时间时,锁自动释放。github

It is possible to make this system more robust modifying the unlock schema as follows:redis

  • 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.dom

更加健壮的释放锁的方式:分布式

  • 设置的 value 是一个随机生成的没法预测的值,叫作 token
  • 再也不使用 DEL 直接删除 key 来释放锁,而是使用一个 script,仅当 value 匹配 token 时才会删除 key

这样能够防止某个客户端在超过失效时间后尝试释放锁,直接使用 DEL 可能会删除掉别的客户端添加的锁。ui

下面是释放锁脚本的例子:this

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
The script should be called with EVAL ...script... 1 resource-name token-value

执行 EVAL ...script... 1 resource-name token-value 命令释放锁。spa

以上是官方文档中的内容,阅读到这里能够发现一个问题:线程

  • 官方的方案中,分布式锁是有个失效时间的,达到失效时间锁会被自动释放,若是此时须要加锁执行的任务还未完成,同时锁又被其余客户端获取到,那么就可能会出现严重的问题;
  • 若是锁不加上失效时间,万一得到锁的客户端忽然 crash 了,没有来得及释放锁,那么这个锁就永远不会被释放。

针对这个问题,能够看下 Redisson 是如何解决的。

Redisson 分布式锁

官方文档:
https://github.com/redisson/r...

经过如下方式,能够得到一个 key 为 myLockRLock 对象:

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");

获取锁和释放锁:

lock.lock(); // 获取锁
try {
  ...
} finally {
  lock.unlock(); // 在 finally 中释放锁
}

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 timeTimeUnit 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 自动清除,锁也就被释放了,不会出现锁永久被占用的状况。

扫码关注个人公众号

扫码关注

相关文章
相关标签/搜索