基於redis的分佈式鎖詳解

在使用分佈式鎖進行互斥資源訪問時候,咱們不少方案是採用redis的實現。
當然,redis的單節點鎖在極端狀況也是有問題的,假設你的業務容許偶爾的失效,使用單節點的redis鎖方案就足夠了,簡單並且效率高。
redis鎖失效的狀況:php

  1. 客戶端1從master節點獲取了鎖
  2. master宕機了,存儲鎖的key還沒來得及同步到slave節點上
  3. slave升級爲master
  4. 客戶端2重新的master上獲取到同一個資源的鎖

因而,客戶端1和客戶端2同事持有了同一個資源的鎖,鎖的安全性被打破。
若是咱們不考慮這種極端狀況,須要實現一個基於單節點redis鎖的大體流程:laravel

set cache_key random_seed NX PX 30000

上面這個set命令拆解開就是:面試

setnx cache_key random_seed 
expire cache_key 30

雖然這兩組命令執行的效果同樣,可是第二個是非原子性操做,若是執行了setnx成功,可是expire失敗的話,就會形成這個key一直存在了,沒法釋放的狀況。redis


redis的做者也指出,在使用單節點redis鎖的時候,設置一個隨機種子做爲key的值是頗有必要的,保證了一個客戶端釋放的鎖必須是本身所持有的那個鎖。假設獲取鎖時set的不是一個隨機數,而是一個固定值,那麼可能會出現下面的狀況:sql

  1. 客戶端1獲取鎖成功
  2. 客戶端1在某個操做上阻塞了很長時間
  3. 過時時間到了,鎖自動釋放(可是在客戶端1看來本身仍是持有鎖中)
  4. 客戶端2獲取到了對應同一個資源的鎖
  5. 客戶端1從阻塞中恢復了,釋放掉本身持有的鎖,也就是釋放掉了客戶端2持有的鎖

客戶端2的鎖被客戶端1是否,失去安全性。
釋放鎖的操做,不少人直接用del命令,這會有很大的問題,保證不了這個key是被加鎖人鎖刪。這時候須要用到隨機數了。釋放鎖的操做有三步:shell

  1. get 所持有鎖
  2. 判斷這個鎖是否本身所持有
  3. 刪除持有鎖

因此,這三步要保證原子性。用lua腳原本執行,redis官方已經提供腳本文件。安全

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

這段腳本在執行的時候,須要把前面的隨機數做爲argv[1] 的值傳進去,把cache_key做爲keys[1]的值傳進去。服務器

public class RedisLockHelper {
    @Resource
    private R2mClusterClient r2mClusterClient;

    /**
     * 相似於setNx的功能,同時設置過時時間爲expire毫秒
     *
     * @param key    加鎖key
     * @param value  確保在加鎖時間內的惟一因子
     * @param expire 過時時間的毫秒數
     * @return
     */
    private String setLock(String key, String value, long expire) {
        return this.set(key, value, "NX", "PX", expire);
    }

    /**
     * 刪除指定key value
     * 若是 r2m中 key 對應的value==value   返回 1
     * 若是 r2m中 key 對應的value!=value   返回 0
     *
     * @param key
     * @return
     */
    private boolean atomDelete(String key, String value) {
        List<String> values = new ArrayList<>();
        values.add(value);
        String sb = "if redis.call('get',KEYS[1])==ARGV[1] then " +
                " return redis.call('del',KEYS[1]) " +
                " else " +
                " return 0" +
                " end";
        if (this.eval(sb, key, values) == 1) {
            return true;
        }
        return false;
    }

    private Long eval(String mobel, String key, List<String> value) {
        return (Long) this.r2mClusterClient.eval(mobel, key, value);
    }

    private String set(String key, String value, String nxxx, String expx, long time) {
        return this.r2mClusterClient.set(key, value, nxxx, expx, time);
    }
}

r2mClusterClient 就是jedis客戶端的封裝。架構

以上內容但願幫助到你們, 不少PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提高,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨須要的能夠免費分享給你們 ,須要戳這裏     PHP進階架構師>>>實戰視頻、大廠面試文檔免費獲取     併發

相關文章
相關標籤/搜索