Lua腳本在redis分佈式鎖場景的運用

redis分佈式鎖,Lua,Lua腳本,lua redis,redis lua 分佈式鎖,redis setnx ,redis分佈式鎖, Lua腳本在redis分佈式鎖場景的運用。java

鎖和分佈式鎖

鎖是什麼?

鎖是一種能夠封鎖資源的東西。這種資源一般是共享的,一般會發生使用競爭的。python

爲何須要鎖?

須要保護共享資源正常使用,不出亂子。
比方說,公司只有一間廁所,這是個共享資源,你們須要共同使用這個廁所,因此避免不了有時候會發生競爭。若是一我的正在使用,另一我的進去了,咋辦呢?若是兩我的同時鑽進了一個廁所,那該怎麼辦?結果如何?誰先用,仍是一塊兒使用?特別的,假如是一男一女同時鑽進了廁所,事情會怎樣呢?反正我是不懂……程序員

若是這個時候廁所門前有個鎖,每一個人都無法隨便進入,而是須要先獲得鎖,才能進去。而獲得這個鎖,就須要裏邊的人先出來。這樣就能夠保證同一時刻,只有一我的在使用廁所,這我的在上廁所的期間不會有不安全的事情發生,不會中途被人闖進來了。redis

Java中的鎖

在 java 編碼的時候,爲了保護共享資源,使得多線程環境下,不會出現「很差的結果」。咱們可使用鎖來進行線程同步。因而咱們能夠根據具體的狀況使用synchronized 關鍵字來修飾一個方法,或者一段代碼。這個方法或者代碼就像是前文中提到的「受保護的廁所,加鎖的廁所」。也可使用 java 5之後的 Lock 來實現,與 synchronized 關鍵字相比,Lock 的使用更靈活,能夠有加鎖超時時間、公平性等優點。算法

分佈式鎖

上面咱們所說的 synchronized 關鍵字也好,Lock 也好。其實他們的做用範圍是啥,就是當前的應用啊。你的代碼在這個 jar 包或者這個 war 包裏邊,被部署在 A 機器上。那麼實際上咱們寫的 synchronized 關鍵字,就是在當前的機器的 JVM在執行代碼的時候發生做用的。假設這個代碼被部署到了三臺機器上 A,B,C。那麼 A 機器中的部署的代碼中的synchronized 關鍵字並不能控制 B,C 中的內容。spring

假如咱們須要在 A,B,C 三臺機器上運行某段程序的時候,實現「原子操做」,synchronized 關鍵字或者 Lock 是不能知足的。很顯然,這個時候咱們須要的鎖,是須要協同這三個節點的,因而,分佈式鎖就須要上場了,他就像是在A,B,C的外面加了一個層,經過它來實現鎖的控制。shell

redis 如何實現加鎖

在redis中,有一條命令,能夠實現相似 「鎖」 的語法是這樣的:編程

SETNX key value

他的做用是,將 key 的值設爲 value ,當且僅當 key 不存在。若給定的 key 已經存在,則 SETNX 不作任何動做。設置成功,返回 1 ;設置失敗,返回 0安全

使用 redis 來實現鎖的邏輯就是這樣的

線程 1 獲取鎖  -- > setnx mylock lockvalue
              -- >  1  獲取鎖成功
線程 2 獲取鎖  -- > setnx mylock lockvalue 
              -- >  0  獲取鎖失敗  (繼續等待,或者其餘邏輯)
線程 1 釋放鎖  -- > 
線程 2 獲取鎖  -- > setnx mylock lockvalue
              -- > 1 獲取成功

鎖超時

在這個例子中,咱們梳理了使用 redis setnx 命令 來實現鎖的邏輯。這裏還須要考慮的是,鎖超時的問題 ,由於當線程 1 獲取了鎖以後,若是業務邏輯執行很長很長時間,那麼其餘線程只能死等,這可不行。因此須要加上超時,結合這些考慮的狀況,實際的 Java 代碼能夠這樣寫:

public static boolean lock(String key,String lockValue,int expire){
        if(null == key){
            return false;
        }
        try {
            Jedis jedis = getJedisPool().getResource();
            String res = jedis.set(key,lockValue,"NX","EX",expire);
            jedis.close();
            return res!=null && res.equals("OK");
        } catch (Exception e) {
            return false;
        }
    }

retry

這裏執行加鎖,不必定能成功。當別人正在持有鎖的時候,加鎖的線程須要繼續嘗試。這個「繼續嘗試」一般是「忙等待」,實現代碼以下:

/**
     * 獲取一個分佈式鎖 , 超時則返回失敗
     * @param key           鎖的key
     * @param lockValue     鎖的value
     * @param timeout       獲取鎖的等待時間,單位爲 秒
     * @return              獲鎖成功 - true | 獲鎖失敗 - false
     */
    public static boolean tryLock(String key,String lockValue,int timeout,int expire){
        final long start = System.currentTimeMillis();
        if(timeout > expiredNx) {
            timeout = expiredNx;
        }
        final long end = start + timeout * 1000;
        boolean res = false; // 默認返回失敗
        while(!(res = lock(key,lockValue,expire))){ // 調用了上面的 lock方法
            if(System.currentTimeMillis() > end) {
                break;
            }
        }
        return res;
    }

redis 如何釋放鎖

根據上面所述,咱們在加鎖的時候執行了:setnx mylock lockvalue , 這種加鎖的本質其實就是 「佔座位」,我把一本書放在自習室第一排的第一個座位上,別人就不能坐了,就得等着我走了,把東西拿走了,他就可使用這個座位了。因此很容易想到,在咱們須要釋放鎖的時候,只須要調用 del mylock 就好了,這樣別的線程想去執行加鎖的時候執行就能夠執行 setnx mylock lockvalue 了。

不應釋放的鎖

可是,直接執行del mylock 是有問題的,咱們不能直接執行 del mylock 爲何?—— 會致使 「信號錯誤」,釋放了不應釋放的鎖 。假設以下場景:

時間線 線程1 線程2 線程3
時刻1 執行 setnx mylock val1 加鎖 執行 setnx mylock val2 加鎖 執行 setnx mylock val2 加鎖
時刻2 加鎖成功 加鎖失敗 加鎖失敗
時刻3 執行任務... 嘗試加鎖... 嘗試加鎖...
時刻4 任務繼續(鎖超時,自動釋放了) setnx 得到了鎖(由於線程1的鎖超時釋放了) 仍然嘗試加鎖...
時刻5 任務完畢,del mylock 釋放鎖 執行任務中... 得到了鎖(由於線程1釋放了線程2的)
...

上面的表格中,有兩個維度,一個是縱向的時間線,一個是橫線的線程併發競爭。咱們能夠發現線程 1 在開始的時候比較幸運,得到了鎖,最早開始執行任務,可是,因爲他比較耗時,最後鎖超時自動釋放了他都還沒執行完。 所以,線程 2 和線程3 的機會來了。而這一輪,線程2 比較幸運,獲得了鎖。但是,當線程2正在執行任務期間,線程1 執行完了,還把線程2的鎖給釋放了。這就至關於,原本你鎖着門在廁所裏邊尿尿,進行到一半的時候,別人進來了,由於他配了一把和你如出一轍的鑰匙!這就亂套了啊

所以,咱們須要安全的釋放鎖——「不是個人鎖,我不能瞎釋放」。因此,咱們在加鎖的時候,就須要標記「這是個人鎖」,在釋放的時候在判斷 「 這是否是個人鎖?」。這裏就須要在釋放鎖的時候加上邏輯判斷,合理的邏輯應該是這樣的:

1. 線程1 準備釋放鎖 , 鎖的key 爲 mylock  鎖的 value 爲 thread1_magic_num
2. 查詢當前鎖 current_value = get mylock
3. 判斷    if current_value == thread1_magic_num -- > 是  我(線程1)的鎖
          else                                   -- >不是 我(線程1)的鎖
4. 是個人鎖就釋放,不然不能釋放(而是執行本身的其餘邏輯)。

爲了實現上面這個邏輯,咱們是沒法經過 redis 自帶的命令直接完成的。若是,再寫複雜的代碼去控制釋放鎖,則會讓總體代碼太過於複雜了。因此,咱們引入了lua腳本。結合Lua 腳本實現釋放鎖的功能,更簡單,redis 執行lua腳本也是原子的,因此更合適,讓合適的人幹合適的事,豈不更好。

經過Lua腳本實現鎖釋放

Lua是啥,Lua是一種功能強大,高效,輕量級,可嵌入的腳本語言。其官方的描述是:

Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.

Lua 調用 redis 很是簡單,而且 Lua 腳本語法也易學,對於有別的編程語言基礎的程序員來講,在不學習Lua腳本語法的狀況下,直接看 Lua 的代碼 也是能夠看懂的。例子以下:

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

上面的代碼,邏輯很簡單,if 中的比較若是是true , 那麼 執行 del 並返回del結果;若是 if 結果爲false 直接返回 0 。這不就知足了咱們釋放鎖的要求嗎?——「 是個人鎖,我就釋放,不是個人鎖,我不能瞎釋放」。

其中的KEYS[1] , ARGV[1] 是參數,咱們只調用 jedis 執行腳本的時候,傳遞這兩個參數就能夠了。

使用redis + lua 來實現釋放鎖的代碼以下:

private static final Long lockReleaseOK = 1L;
static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua腳本,用來釋放分佈式鎖

public static boolean releaseLock(String key ,String lockValue){
    if(key == null || lockValue == null) {
        return false;
    }
    try {
        Jedis jedis = getJedisPool().getResource();
        Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue));
        jedis.close();
        return res!=null && res.equals(lockReleaseOK);
    } catch (Exception e) {
        return false;
    }
}

如此,咱們便實現了鎖的安全釋放。同時,咱們還須要結合業務邏輯,進行具體健壯性的保證,好比若是結束了必定不能忘記釋放鎖,異常了也要釋放鎖,某種狀況下是否須要回滾事務等。總結這個分佈式鎖使用的過程即是:

  • 加鎖時 key 同,value 不一樣。
  • 釋放鎖時,根據value判斷,是否是個人鎖,不能釋放別人的鎖。
  • 及時釋放鎖,而不是利用自動超時。
  • 鎖超時時間必定要結合業務狀況權衡,過長,太短都不行。
  • 程序異常之處,要捕獲,並釋放鎖。若是須要回滾的,主動作回滾、補償。保證總體的健壯性,一致性。

用redis作分佈式鎖真的靠譜嗎

上面的文字中,咱們討論如何使用redis做爲分佈式鎖,並討論了一些細節問題,如鎖超時的問題、安全釋放鎖的問題。目前爲止,彷佛很完美的解決的咱們想要的分佈式鎖功能。然而事情並無這麼簡單,用redis作分佈式鎖並不「靠譜」。

不靠譜的狀況

上面咱們說的是redis,是單點的狀況。若是是在redis sentinel集羣中狀況就有所不一樣了。關於redis sentinel 集羣能夠看這裏。在redis sentinel集羣中,咱們具備多臺redis,他們之間有着主從的關係,例如一主二從。咱們的set命令對應的數據寫到主庫,而後同步到從庫。當咱們申請一個鎖的時候,對應就是一條命令 setnx mykey myvalue ,在redis sentinel集羣中,這條命令先是落到了主庫。假設這時主庫down了,而這條數據還沒來得及同步到從庫,sentinel將從庫中的一臺選舉爲主庫了。這時,咱們的新主庫中並無mykey這條數據,若此時另一個client執行 setnx mykey hisvalue , 也會成功,即也能獲得鎖。這就意味着,此時有兩個client得到了鎖。這不是咱們但願看到的,雖然這個狀況發生的記錄很小,只會在主從failover的時候纔會發生,大多數狀況下、大多數系統均可以容忍,可是不是全部的系統都能容忍這種瑕疵。

redlock

爲了解決故障轉移狀況下的缺陷,Antirez 發明了 Redlock 算法,使用redlock算法,須要多個redis實例,加鎖的時候,它會想多半節點發送 setex mykey myvalue 命令,只要過半節點成功了,那麼就算加鎖成功了。釋放鎖的時候須要想全部節點發送del命令。這是一種基於【大多數都贊成】的一種機制。感興趣的能夠查詢相關資料。在實際工做中使用的時候,咱們能夠選擇已有的開源實現,python有redlock-py,java 中有Redisson redlock。

redlock確實解決了上面所說的「不靠譜的狀況」。可是,它解決問題的同時,也帶來了代價。你須要多個redis實例,你須要引入新的庫 代碼也得調整,性能上也會有損。因此,果真是不存在「完美的解決方案」,咱們更須要的是可以根據實際的狀況和條件把問題解決了就好。

至此,我大體講清楚了redis分佈式鎖方面的問題(往後若是有新的領悟就繼續更新)。

redis單點、redis主從、redis集羣cluster配置搭建與使用

Netty開發redis客戶端,Netty發送redis命令,netty解析redis消息

spring如何啓動的?這裏結合spring源碼描述了啓動過程

SpringMVC是怎麼工做的,SpringMVC的工做原理

spring 異常處理。結合spring源碼分析400異常處理流程及解決方法

Mybatis Mapper接口是如何找到實現類的-源碼分析

使用Netty實現HTTP服務器

Netty實現心跳機制

Netty系列

Lua腳本在redis分佈式鎖場景的運用

CORS詳解,CORS原理分析

相關文章
相關標籤/搜索