基於redis實現的分布式鎖


基於redis實現的分布式鎖實現

什么是鎖

這里需要引入一個話題,什么是鎖?其實說到鎖在我們的現實生活中非常的常見,比如密碼鎖,指紋鎖,他是為了保證家中物資的安全性的一道保障。而在我們的計算機領域中,其實也有鎖的概念,他的目的與上相似,都是為了保證數據的最終一致性,當然在單個線程鎖是沒有太大作用,但是若出現多個線程之間對某個資源進行競爭的時候,那么鎖的存在就很有意義。那么剛才所提到的是針對與單體服務的鎖(這個有時間可以講講基於java中鎖的概念),然而隨網絡用戶量的升級,單體服務難以支撐起龐大的訪問量,為此我們的服務之間存在了數據以及模塊的拆分,之前關於單體服務的鎖對我們就不太適用了,因此就會引入到我們今天所需要講的分布式鎖。

 

為什么要用分布式鎖

就如之前所說,我們需要保證數據的一致性,防止分布式系統中多個線程之間相互進行干擾,我們需要一種分布式協調技術來對這些進程進行調度。而這個分布式協調技術的核心就是來實現這個分布式鎖,以用戶購買商品下單為例子。

  • 未使用鎖

    我們在沒有使用任何鎖的時候,當用戶進行下單的時候我們需要對redis 中的庫存進行查詢,如果是可以售賣,那么數量就需要進行更新,同時使用mq進行落庫。但是,這個地方需要注意的是查詢和更新是兩部操作(其實可以 使用lua進行原子操作,但是今天主要講的是分布式鎖) 這樣可能會存在bug,如果兩個線程同時進行操作的時候,如果庫存只有1,兩個線程在沒有執行第三步的間隙同時進入了第二階段,都認為可以購買,並進入了第四階段。 那么就會存在我們所謂的超賣問題(沒錯的話,估計會被請去喝茶了)

      

  • 使用分布式鎖

    這里略過了多個單體服務器多線程操作的過程,其實也和上面的類似,都是相同時間,多個線程對同一數據進行操作。其實有個思路,就是能夠保證全局唯一線程去獲取到鎖並對數據進行操作,那么就可以保證全局的安全性問題,能夠實現分布式鎖的框架很多,如 redis,以及zookeeper甚至是數據庫, 我們這里使用redis作為我們的分布式鎖,關於redis為什么能實現分布式鎖主要是用到了他的單線程模式,采用隊列模式將並發訪問轉換成串行模式

 

在圖中其實可以看到,如果多個服務進行購買商品的時候,在最后會進入到分布式鎖的階段,得到了鎖的服務器的那個線程才能執行對扣減操作。其實對應鎖的和java中的多線程非常類似,只是從java中的synchronized的鎖更改未了redis 的單線程操作,然后操作對象由原來的單體服務器中的多個線程更改為了多個服務器的多個線程進行執行操作。

 

分布式鎖原理

既然redis那么好用,那是用的內部哪個命令呢,setNx,沒錯。就是一個命令就可以做到我們想要的。他其實內部包含有兩部分操作

1、判斷數據是否存在,

2、如果存在那么不插入數據,如果不存在那么就插入對應的數據。

 

具體的執行操作如下。

img

 

 

圖中的Setex 命令為指定的 key 設置值及其過期時間。如果 key 已經存在, SETEX 命令將會替換舊的值。

同時這里需要注意

* 【千萬記住】解鎖流程不能遺漏,否則導致任務執行一次就永不過期

* 將加鎖代碼和任務邏輯放在try,catch代碼塊,將解鎖流程放在finally

 

 

分布式鎖可能出現的問題

雖然這個命令能夠完成我們在高並發的數據一致性,但是還是可能會存在一些問題

  • 服務宕機導致redis鎖永不失效

  • 線程誤刪除redis鎖

  • 執行線程操作時redis鎖過期

  • 集群模式下哨兵重選導致的redis丟失

     

所以,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

  1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。

  2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。

  3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。

  4. 解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。

我們通過代碼的方式來對分布式鎖出現問題進行節點

1、服務宕機導致redis鎖永不失效

  • 錯誤代碼范例


    public static void thisIsWrongLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

      Long result = jedis.setnx(lockKey, requestId);
      if (result == 1) {
          // 若在這里程序突然崩潰,則無法設置過期時間,將發生死鎖
          jedis.expire(lockKey, expireTime);
      }

    }

    由於設置中 加鎖以及設置過期時間為兩段操作,在未執行過期時間時服務器宕機,那么這個鎖就永遠沒有辦法失效了

  • 正確操作

    其實正確操作就是將剛才的兩步操作,更改為一步操作,那么操作的方式是什么呢,這就要涉及到另外個語言LUA腳本語言,他執行是原子性質操作。

      //上鎖腳本
      private static final String LOCK_LUA = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 'true' else return 'false' end";
     
      /**
        *
        * @param lockKey 上鎖
        * @param time 上鎖時間
        * @return
        */
        public boolean lock(String lockKey, int time) {
          RedisScript lockRedisScript = RedisScript.of(LOCK_LUA, String.class);
          List<String> keys = Collections.singletonList(lockKey);
          /**
          * 這里上鎖的value是當前線程
          **/
          String flag = redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, keys, Thread.currentThread().getName(), String.valueOf(time));
          return Boolean.valueOf(flag);
      }


 

2、線程誤刪除redis鎖

 

  • 錯誤示范



      public   void wrongReleaseLock1( String lockKey) {
          redisTemplate.delete(lockKey);
      }

    沒錯 看了這個就是直接強制刪除對應的redis key值,管你是誰,直接強刪,這樣也會出現很多問題。

 

  • 正確示范

    我們可以在進行操作的時候來對key值數據進行判斷,判斷數據是否是我們之前存放的結果值,一般來結果值也需要一個特定的數據,那想像下,在同一時間執行如何他設置一個唯一性id的value值呢?其實方式也很多,redis的incr或者雪花算法生成的唯一性id,然后使用lua腳本進行執行操作就可以了

         //解鎖腳本
      private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) end return 'true' ";
     
       
      public void unlock(String lockKey, String val) {
          RedisScript unLockRedisScript = RedisScript.of(UNLOCK_LUA, String.class);
          List<String> keys = Collections.singletonList(LOCK_PREFIX + lockKey);
          redisTemplate.execute(unLockRedisScript, argsSerializer, resultSerializer, keys, val);
      }

 

3、 redis鎖提前過期

在生產過程中其實可能會出現這樣業務代碼執行緩慢的情況,而我們在添加的redis的過期太短,導致程序還沒有執行完,redis就直接過期從而其他線程會提前拿到對應的鎖。針對與解決思路其實設置時間長一點也行,但這里其實可以考慮對鎖進行自動續期,需要引入redission客戶端進行操作。它內部提供了對應的看門狗,作用是在redisson實例被關閉之前,不斷的對鎖進行延長時間。

 

redisson的底層原理

img

 

 

4、集群模式下哨兵重選導致的redis丟失

如果redis 部署的是集群版本可能會腦裂或者是主master宕機問題。那么就很有可能會出現鎖的丟失:

  1. 客戶端1在Redis的master節點上拿到了鎖

  2. Master宕機了,存儲鎖的key還沒有來得及同步到Slave上

  3. master故障,發生故障轉移,slave節點升級為master節點

  4. 客戶端2從新的Master獲取到了對應同一個資源的鎖

於是,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破了。針對這個問題。Redis作者antirez提出了RedLock算法來解決這個問題

redLock思路

大致思路如下

1、獲取當前時間毫秒值 CT

2、按照順序向N個節點中執行獲取鎖的操作,為了保證在某個在某個Redis節點不可用的時候算法能夠繼續運行,這個獲取鎖的操作還需要一個超時時間。它應該遠小於鎖的過期時間(expireTime))。客戶端向某個Redis節點獲取鎖失敗后,應立即嘗試下一個Redis節點。這里失敗包括Redis節點不可用或者該Redis節點上的鎖已經被其他客戶端持有。

3、計算總耗時間,即ET=now()-CT,然后與過期時間進行比對,如果是小於鎖過期則對應上鎖生效,否則認定失敗,同時對所有節點進行鎖的刪除(無論是否得到都得執行該操作)

 

當然,這里有個小問題:一定是要全部節點獲取到才認為上鎖成功么?

其實當超過半數redis請求到鎖的時候,才算是真正獲取到了鎖。如果沒有獲取到鎖,則把部分已鎖的redis釋放掉。

 

 

今天的分享就到這里了,下期有興趣可以給大家分享下分布式事務的小知識。

 

 

附錄:

 

紅鎖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM