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 自動清除,鎖也就被釋放了,不會出現鎖永久被佔用的狀況。

掃碼關注個人公衆號

掃碼關注

相關文章
相關標籤/搜索