乐趣区

关于java:Redis-分布式锁原理及-Redisson-实现

Redis 分布式锁原理

Redis 分布式锁原理,能够间接看官网文档:
https://redis.io/commands/set…

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

当达到生效工夫时,锁主动开释。

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 可能会删除掉别的客户端增加的锁。

上面是开释锁脚本的例子:

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 命令开释锁。

以上是官网文档中的内容,浏览到这里能够发现一个问题:

  • 官网的计划中,分布式锁是有个生效工夫的,达到生效工夫锁会被主动开释,如果此时须要加锁执行的工作还未实现,同时锁又被其余客户端获取到,那么就可能会呈现重大的问题;
  • 如果锁不加上生效工夫,万一取得锁的客户端忽然 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 主动革除,锁也就被开释了,不会呈现锁永恒被占用的状况。

扫码关注我的公众号

退出移动版