什麼是分散式鎖?幾種分散式鎖分別是怎麼實現的?

推薦學習

  • 1:

    一、什麼是分散式鎖:

    1、什麼是分散式鎖:

    分散式鎖,即分散式系統中的鎖。在單體應用中我們通過鎖解決的是控制共享資源訪問的問題,而分散式鎖,就是解決了分散式系統中控制共享資源訪問的問題。與單體應用不同的是,分散式系統中競爭共享資源的最小粒度從線程升級成了進程。

    2、分散式鎖應該具備哪些條件:

    • 在分散式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行
    • 高可用的獲取鎖與釋放鎖
    • 高性能的獲取鎖與釋放鎖
    • 具備可重入特性(可理解為重新進入,由多於一個任務並發使用,而不必擔心數據錯誤)
    • 具備鎖失效機制,即自動解鎖,防止死鎖
    • 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗

    3、分散式鎖的實現方式:

    基於資料庫實現分散式鎖基於zookeeper實現分散式鎖基於reids實現分散式鎖

    這篇文章就簡單介紹下這幾種分散式鎖的實現,重點講解的是基於redis的分散式鎖。

    二、基於資料庫的分散式鎖:

    基於資料庫的鎖實現也有兩種方式,一是基於資料庫表的增刪,另一種是基於資料庫排他鎖。

    1、基於資料庫表的增刪:

    基於資料庫表增刪是最簡單的方式,首先創建一張鎖的表主要包含下列欄位:類的全路徑名+方法名,時間戳等欄位。

    具體的使用方式:當需要鎖住某個方法時,往該表中插入一條相關的記錄。類的全路徑名+方法名是有唯一性約束的,如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個操作可以成功,那麼我們就認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。執行完畢之後,需要delete該記錄。

    (這裡只是簡單介紹一下,對於上述方案可以進行優化,如:應用主從資料庫,數據之間雙向同步;一旦掛掉快速切換到備庫上;做一個定時任務,每隔一定時間把資料庫中的超時數據清理一遍;使用while循環,直到insert成功再返回成功;記錄當前獲得鎖的機器的主機信息和線程信息,下次再獲取鎖的時候先查詢資料庫,如果當前機器的主機信息和線程信息在資料庫可以查到的話,直接把鎖分配給他就可以了,實現可重入鎖)

    2、基於資料庫排他鎖:

    基於MySqlInnoDB引擎,可以使用以下方法來實現加鎖操作:

    public void lock(){
        connection.setAutoCommit(false)
        int count = 0;
        while(count < 4){
            try{
                select * from lock where lock_name=xxx for update;
                if(結果不為空){
                    //代表獲取到鎖
                    return;
                }
            }catch(Exception e){
     
            }
            //為空或者拋異常的話都表示沒有獲取到鎖
            sleep(1000);
            count++;
        }
        throw new LockException();
    }

    在查詢語句後面增加for update,資料庫會在查詢過程中給資料庫表增加排他鎖。獲得排它鎖的線程即可獲得分散式鎖,當獲得鎖之後,可以執行方法的業務邏輯,執行完方法之後,釋放鎖connection.commit()。當某條記錄被加上排他鎖之後,其他線程無法獲取排他鎖並被阻塞。

    3、基於資料庫鎖的優缺點:

    上面兩種方式都是依賴資料庫表,一種是通過表中的記錄判斷當前是否有鎖存在,另外一種是通過資料庫的排他鎖來實現分散式鎖。

    • 優點是直接藉助資料庫,簡單容易理解。
    • 缺點是操作資料庫需要一定的開銷,性能問題需要考慮。

    三、基於Zookeeper的分散式鎖

    基於zookeeper臨時有序節點可以實現的分散式鎖。每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。 (第三方庫有 Curator,Curator提供的InterProcessMutex是分散式鎖的實現)

    Zookeeper實現的分散式鎖存在兩個個缺點:

    • (1)性能上可能並沒有緩存服務那麼高,因為每次在創建鎖和釋放鎖的過程中,都要動態創建、銷毀瞬時節點來實現鎖功能。ZK中創建和刪除節點只能通過Leader伺服器來執行,然後將數據同步到所有的Follower機器上。
    • (2)zookeeper的並發安全問題:因為可能存在網路抖動,客戶端和ZK集群的session連接斷了,zk集群以為客戶端掛了,就會刪除臨時節點,這時候其他客戶端就可以獲取到分散式鎖了。

    四、基於redis的分散式鎖:

    Redis命令說明:

    (1)setnx命令:set if not exists,當且僅當 key 不存在時,將 key 的值設為 value。若給定的 key 已經存在,則 SETNX 不做任何動作。

    • 返回1,說明該進程獲得鎖,將 key 的值設為 value
    • 返回0,說明其他進程已經獲得了鎖,進程不能進入臨界區

    命令格式:setnx lock.key lock.value

    (2)get命令:獲取key的值,如果存在,則返回;如果不存在,則返回nil

    命令格式:get lock.key

    (3)getset命令:該方法是原子的,對key設置newValue這個值,並且返回key原來的舊值。

    命令格式:getset lock.key newValue

    (4)del命令:刪除redis中指定的key

    命令格式:del lock.key

    方案一:基於set命令的分散式鎖

    1、加鎖:使用setnx進行加鎖,當該指令返回1時,說明成功獲得鎖

    2、解鎖:當得到鎖的線程執行完任務之後,使用del命令釋放鎖,以便其他線程可以繼續執行setnx命令來獲得鎖

    (1)存在的問題:假設線程獲取了鎖之後,在執行任務的過程中掛掉,來不及顯示地執行del命令釋放鎖,那麼競爭該鎖的線程都會執行不了,產生死鎖的情況。

    (2)解決方案:設置鎖超時時間

    3、設置鎖超時時間:setnx 的 key 必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。可以使用expire命令設置鎖超時時間

    (1)存在問題:

    setnx 和 expire 不是原子性的操作,假設某個線程執行setnx 命令,成功獲得了鎖,但是還沒來得及執行expire 命令,伺服器就掛掉了,這樣一來,這把鎖就沒有設置過期時間了,變成了死鎖,別的線程再也沒有辦法獲得鎖了。

    (2)解決方案:redis的set命令支持在獲取鎖的同時設置key的過期時間

    4、使用set命令加鎖並設置鎖過期時間:

    命令格式:set <lock.key> <lock.value> nx ex <expireTime>

    詳情參考redis使用文檔:
    http://doc.redisfans.com/string/set.html

    (1)存在問題:

    ① 假如線程A成功得到了鎖,並且設置的超時時間是 30 秒。如果某些原因導致線程 A 執行的很慢,過了 30 秒都沒執行完,這時候鎖過期自動釋放,線程 B 得到了鎖。

    ② 隨後,線程A執行完任務,接著執行del指令來釋放鎖。但這時候線程 B 還沒執行完,線程A實際上刪除的是線程B加的鎖。

    (2)解決方案:

    可以在 del 釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。在加鎖的時候把當前的線程 ID 當做value,並在刪除之前驗證 key 對應的 value 是不是自己線程的 ID。但是,這樣做其實隱含了一個新的問題,get操作、判斷和釋放鎖是兩個獨立操作,不是原子性。對於非原子性的問題,我們可以使用lua腳本來確保操作的原子性

    5、鎖續期:(這種機制類似於Redisson的看門狗機制,文章後面會詳細說明)

    雖然步驟4避免了線程A誤刪掉key的情況,但是同一時間有 A,B 兩個線程在訪問代碼塊,仍然是不完美的。怎麼辦呢?我們可以讓獲得鎖的線程開啟一個守護線程,用來給快要過期的鎖「續期」。

    ① 假設線程A執行了29 秒後還沒執行完,這時候守護線程會執行 expire 指令,為這把鎖續期 20 秒。守護線程從第 29 秒開始執行,每 20 秒執行一次。

    ② 情況一:當線程A執行完任務,會顯式關掉守護線程。

    ③ 情況二:如果伺服器忽然斷電,由於線程 A 和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。

    方案二:基於setnx、get、getset的分散式鎖

    1、實現原理:

    (1)setnx(lockkey, 當前時間+過期超時時間) ,如果返回1,則獲取鎖成功;如果返回0則沒有獲取到鎖,轉向步驟(2)

    (2)get(lockkey)獲取值oldExpireTime ,並將這個value值與當前的系統時間進行比較,如果小於當前系統時間,則認為這個鎖已經超時,可以允許別的請求重新獲取,轉向步驟(3)

    (3)計算新的過期時間 newExpireTime=當前時間+鎖超時時間,然後getset(lockkey, newExpireTime) 會返回當前lockkey的值currentExpireTime

    (4)判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,說明當前getset設置成功,獲取到了鎖。如果不相等,說明這個鎖又被別的請求獲取走了,那麼當前請求可以直接返回失敗,或者繼續重試。

    (5)在獲取到鎖之後,當前線程可以開始自己的業務處理,當處理完畢後,比較自己的處理時間和對於鎖設置的超時時間,如果小於鎖設置的超時時間,則直接執行del命令釋放鎖(釋放鎖之前需要判斷持有鎖的線程是不是當前線程);如果大於鎖設置的超時時間,則不需要再鎖進行處理。

    2、代碼實現:

    (1)獲取鎖的實現方式:

    public boolean lock(long acquireTimeout, TimeUnit timeUnit) throws InterruptedException {
        acquireTimeout = timeUnit.toMillis(acquireTimeout);
        long acquireTime = acquireTimeout + System.currentTimeMillis();
        //使用J.U.C的ReentrantLock
        threadLock.tryLock(acquireTimeout, timeUnit);
        try {
        	//循環嘗試
            while (true) {
            	//調用tryLock
                boolean hasLock = tryLock();
                if (hasLock) {
                    //獲取鎖成功
                    return true;
                } else if (acquireTime < System.currentTimeMillis()) {
                    break;
                }
                Thread.sleep(sleepTime);
            }
        } finally {
            if (threadLock.isHeldByCurrentThread()) {
                threadLock.unlock();
            }
        }
     
        return false;
    }
     
    public boolean tryLock() {
     
        long currentTime = System.currentTimeMillis();
        String expires = String.valueOf(timeout + currentTime);
        //設置互斥量
        if (redisHelper.setNx(mutex, expires) > 0) {
        	//獲取鎖,設置超時時間
            setLockStatus(expires);
            return true;
        } else {
            String currentLockTime = redisUtil.get(mutex);
            //檢查鎖是否超時
            if (Objects.nonNull(currentLockTime) && Long.parseLong(currentLockTime) < currentTime) {
                //獲取舊的鎖時間並設置互斥量
                String oldLockTime = redisHelper.getSet(mutex, expires);
                //舊值與當前時間比較
                if (Objects.nonNull(oldLockTime) && Objects.equals(oldLockTime, currentLockTime)) {
                	//獲取鎖,設置超時時間
                    setLockStatus(expires);
                    return true;
                }
            }
     
            return false;
        }
    }

    tryLock方法中,主要邏輯如下:lock調用tryLock方法,參數為獲取的超時時間與單位,線程在超時時間內,獲取鎖操作將自旋在那裡,直到該自旋鎖的保持者釋放了鎖。

    (2)釋放鎖的實現方式:

    public boolean unlock() {
        //只有鎖的持有線程才能解鎖
        if (lockHolder == Thread.currentThread()) {
            //判斷鎖是否超時,沒有超時才將互斥量刪除
            if (lockExpiresTime > System.currentTimeMillis()) {
                redisHelper.del(mutex);
                logger.info("刪除互斥量[{}]", mutex);
            }
            lockHolder = null;
            logger.info("釋放[{}]鎖成功", mutex);
     
            return true;
        } else {
            throw new IllegalMonitorStateException("沒有獲取到鎖的線程無法執行解鎖操作");
        }
    }

    存在問題:

    (1)這個鎖的核心是基於System.currentTimeMillis(),如果多台伺服器時間不一致,那麼問題就出現了,但是這個bug完全可以從伺服器運維層面規避的,而且如果伺服器時間不一樣的話,只要和時間相關的邏輯都是會出問題的

    (2)如果前一個鎖超時的時候,剛好有多台伺服器去請求獲取鎖,那麼就會出現同時執行redis.getset()而導致出現過期時間覆蓋問題,不過這種情況並不會對正確結果造成影響

    (3)存在多個線程同時持有鎖的情況:如果線程A執行任務的時間超過鎖的過期時間,這時另一個線程就可以獲得這個鎖了,造成多個線程同時持有鎖的情況。類似於方案一,可以使用「鎖續期」的方式來解決。

    前兩種redis分散式鎖的存在的問題

    前面兩種redis分散式鎖的實現方式,如果從「高可用」的層面來看,仍然是有所欠缺,也就是說當 redis 是單點的情況下,當發生故障時,則整個業務的分散式鎖都將無法使用。

    為了提高可用性,我們可以使用主從模式或者哨兵模式,但在這種情況下仍然存在問題,在主從模式或者哨兵模式下,正常情況下,如果加鎖成功了,那麼master節點會非同步複製給對應的slave節點。但是如果在這個過程中發生master節點宕機,主備切換,slave節點從變為了 master節點,而鎖還沒從舊master節點同步過來,這就發生了鎖丟失,會導致多個客戶端可以同時持有同一把鎖的問題。來看個圖來想下這個過程:

    那麼,如何避免這種情況呢?redis 官方給出了基於多個 redis 集群部署的高可用分散式鎖解決方案:RedLock,在方案三我們就來詳細介紹一下。(備註:如果master節點宕機期間,可以容忍多個客戶端同時持有鎖,那麼就不需要redLock)

    方案三:基於RedLock的分散式鎖

    redLock的官方文檔地址:
    https://redis.io/topics/distlock

    Redlock演算法是Redis的作者 Antirez 在單Redis節點基礎上引入的高可用模式。Redlock的加鎖要結合單節點分散式鎖演算法共同實現,因為​​​它是RedLock的基礎

    1、加鎖實現原理:

    現在假設有5個Redis主節點(大於3的奇數個),這樣基本保證他們不會同時都宕掉,獲取鎖和釋放鎖的過程中,客戶端會執行以下操作:

    (1)獲取當前unix時間,以毫秒為單位,並設置超時時間TTL

    TTL 要大於 正常業務執行的時間 + 獲取所有redis服務消耗時間 + 時鐘漂移

    (2)依次嘗試從5個實例,使用相同的key和具有唯一性的value獲取鎖,當向Redis請求獲取鎖時,客戶端應該設置一個網路連接和響應超時時間,這個超時時間應該小於鎖的失效時間TTL,這樣可以避免客戶端死等。比如:TTL為5s,設置獲取鎖最多用1s,所以如果一秒內無法獲取鎖,就放棄獲取這個鎖,從而嘗試獲取下個鎖

    (3)客戶端 獲取所有能獲取的鎖後的時間 減去 第(1)步的時間,就得到鎖的獲取時間。鎖的獲取時間要小於鎖失效時間TTL,並且至少從半數以上的Redis節點取到鎖,才算獲取成功鎖

    (4)如果成功獲得鎖,key的真正有效時間 = TTL - 鎖的獲取時間 - 時鐘漂移。比如:TTL 是5s,獲取所有鎖用了2s,則真正鎖有效時間為3s

    (5)如果因為某些原因,獲取鎖失敗(沒有在半數以上實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖,無論Redis實例是否加鎖成功,因為可能服務端響應消息丟失了但是實際成功了。

    設想這樣一種情況:客戶端發給某個Redis節點的獲取鎖的請求成功到達了該Redis節點,這個節點也成功執行了SET操作,但是它返回給客戶端的響應包卻丟失了。這在客戶端看來,獲取鎖的請求由於超時而失敗了,但在Redis這邊看來,加鎖已經成功了。因此,釋放鎖的時候,客戶端也應該對當時獲取鎖失敗的那些Redis節點同樣發起請求。實際上,這種情況在非同步通信模型中是有可能發生的:客戶端向伺服器通信是正常的,但反方向卻是有問題的。

    (6)失敗重試:當client不能獲取鎖時,應該在隨機時間後重試獲取鎖;同時重試獲取鎖要有一定次數限制;

    在隨機時間後進行重試,主要是防止過多的客戶端同時嘗試去獲取鎖,導致彼此都獲取鎖失敗的問題。

    演算法示意圖如下:

    2、RedLock性能及崩潰恢復的相關解決方法:

    由於N個Redis節點中的大多數能正常工作就能保證Redlock正常工作,因此理論上它的可用性更高。前面我們說的主從架構下存在的安全性問題,在RedLock中已經不存在了,但如果有節點發生崩潰重啟,還是會對鎖的安全性有影響的,具體的影響程度跟Redis持久化配置有關:

    (1)如果redis沒有持久化功能,在clientA獲取鎖成功後,所有redis重啟,clientB能夠再次獲取到鎖,這樣違法了鎖的排他互斥性;

    (2)如果啟動AOF永久化存儲,事情會好些, 舉例:當我們重啟redis後,由於redis過期機制是按照unix時間戳走的,所以在重啟後,然後會按照規定的時間過期,不影響業務;但是由於AOF同步到磁碟的方式默認是每秒一次,如果在一秒內斷電,會導致數據丟失,立即重啟會造成鎖互斥性失效;但如果同步磁碟方式使用Always(每一個寫命令都同步到硬碟)造成性能急劇下降;所以在鎖完全有效性和性能方面要有所取捨;

    (3)為了有效解決既保證鎖完全有效性 和 性能高效問題:antirez又提出了「延遲重啟」的概念,redis同步到磁碟方式保持默認的每秒1次,在redis崩潰單機後(無論是一個還是所有),先不立即重啟它,而是等待TTL時間後再重啟,這樣的話,這個節點在重啟前所參與的鎖都會過期,它在重啟後就不會對現有的鎖造成影響,缺點是在TTL時間內服務相當於暫停狀態;

    3、redisson中RedLock的實現:

    在JAVA的redisson包已經實現了對RedLock的封裝,主要是通過 redisclient 與 lua 腳本實現的,之所以使用 lua 腳本,是為了實現加解鎖校驗與執行的事務性。

    (1)唯一ID的生成:

    分散式事務鎖中,為了能夠讓作為中心節點的存儲節點獲取鎖的持有者,從而避免鎖被非持有者誤解鎖,每個發起請求的 client 節點都必須具有全局唯一的 id。通常我們是使用 UUID 來作為這個唯一 id,redisson 也是這樣實現的,在此基礎上,redisson 還加入了 threadid 避免了多個線程反覆獲取 UUID 的性能損耗

    protected final UUID id = UUID.randomUUID();
    String getLockName(long threadId) {
    	return id + ":" + threadId;
    }

    (2)加鎖邏輯:

    redisson 加鎖的核心代碼非常容易理解,通過傳入 TTL 與唯一 id,實現一段時間的加鎖請求。下面是可重入鎖的實現邏輯:

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) 
    {
    	internalLockLeaseTime = unit.toMillis(leaseTime);
     
    	// 獲取鎖時向5個redis實例發送的命令
    	return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
    			  // 校驗分散式鎖的KEY是否已存在,如果不存在,那麼執行hset命令(hset REDLOCK_KEY uuid+threadId 1),並通過pexpire設置失效時間(也是鎖的租約時間)
    			  "if (redis.call('exists', KEYS[1]) == 0) then " +
    				  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
    				  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    				  "return nil; " +
    			  "end; " +
    			  // 如果分散式鎖的KEY已存在,則校驗唯一 id,如果唯一 id 匹配,表示是當前線程持有的鎖,那麼重入次數加1,並且設置失效時間
    			  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
    				  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
    				  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    				  "return nil; " +
    			  "end; " +
    			  // 獲取分散式鎖的KEY的失效時間毫秒數
    			  "return redis.call('pttl', KEYS[1]);",
    			  // KEYS[1] 對應分散式鎖的 key;ARGV[1] 對應 TTL;ARGV[2] 對應唯一 id
    				Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

    (3)釋放鎖邏輯:

    protected RFuture<Boolean> unlockInnerAsync(long threadId) 
    {
    	// 向5個redis實例都執行如下命令
    	return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
    			// 如果分散式鎖 KEY 不存在,那麼向 channel 發布一條消息
    			"if (redis.call('exists', KEYS[1]) == 0) then " +
    				"redis.call('publish', KEYS[2], ARGV[1]); " +
    				"return 1; " +
    			"end;" +
    			// 如果分散式鎖存在,但是唯一 id 不匹配,表示鎖已經被佔用
    			"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
    				"return nil;" +
    			"end; " +
    			// 如果就是當前線程佔有分散式鎖,那麼將重入次數減 1
    			"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
    			// 重入次數減1後的值如果大於0,表示分散式鎖有重入過,那麼只設置失效時間,不刪除
    			"if (counter > 0) then " +
    				"redis.call('pexpire', KEYS[1], ARGV[2]); " +
    				"return 0; " +
    			"else " +
    				// 重入次數減1後的值如果為0,則刪除鎖,並發布解鎖消息
    				"redis.call('del', KEYS[1]); " +
    				"redis.call('publish', KEYS[2], ARGV[1]); " +
    				"return 1; "+
    			"end; " +
    			"return nil;",
    			// KEYS[1] 表示鎖的 key,KEYS[2] 表示 channel name,ARGV[1] 表示解鎖消息,ARGV[2] 表示 TTL,ARGV[3] 表示唯一 id
    			Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
    }

    (4)redisson中RedLock的使用:

    Config config = new Config();
    config.useSentinelServers()
            .addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
    		.setMasterName("masterName")
    		.setPassword("password").setDatabase(0);
     
    RedissonClient redissonClient = Redisson.create(config);
    RLock redLock = redissonClient.getLock("REDLOCK_KEY");
     
    try {
        // 嘗試加鎖,最多等待500ms,上鎖以後10s自動解鎖
    	boolean isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    	if (isLock) {
    		//獲取鎖成功,執行對應的業務邏輯
    	}
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
    	redLock.unlock();
    }

    可以看到,redisson 包的實現中,通過 lua 腳本校驗了解鎖時的 client 身份,所以我們無需再在 finally 中去判斷是否加鎖成功,也無需做額外的身份校驗,可以說已經達到開箱即用的程度了。

    同樣,基於RedLock實現的分散式鎖也存在 client 獲取鎖之後,在 TTL 時間內沒有完成業務邏輯的處理,而此時鎖會被自動釋放,造成多個線程同時持有鎖的問題。而Redisson 在實現的過程中,自然也考慮到了這一問題,redisson 提供了一個「看門狗」的特性,當鎖即將過期還沒有釋放時,不斷的延長鎖key的生存時間。(具體實現原理會在方案四進行介紹)

    方案四:基於Redisson看門狗的分散式鎖

    前面說了,如果某些原因導致持有鎖的線程在鎖過期時間內,還沒執行完任務,而鎖因為還沒超時被自動釋放了,那麼就會導致多個線程同時持有鎖的現象出現,而為了解決這個問題,可以進行「鎖續期」。其實,在JAVA的Redisson包中有一個"看門狗"機制,已經幫我們實現了這個功能。

    1、redisson原理:

    redisson在獲取鎖之後,會維護一個看門狗線程,當鎖即將過期還沒有釋放時,不斷的延長鎖key的生存時間

    2、加鎖機制:

    線程去獲取鎖,獲取成功:執行lua腳本,保存數據到redis資料庫。

    線程去獲取鎖,獲取失敗:一直通過while循環嘗試獲取鎖,獲取成功後,執行lua腳本,保存數據到redis資料庫。

    3、watch dog自動延期機制:

    看門狗啟動後,對整體性能也會有一定影響,默認情況下看門狗線程是不啟動的。如果使用redisson進行加鎖的同時設置了鎖的過期時間,也會導致看門狗機制失效。

    redisson在獲取鎖之後,會維護一個看門狗線程,在每一個鎖設置的過期時間的1/3處,如果線程還沒執行完任務,則不斷延長鎖的有效期。看門狗的檢查鎖超時時間默認是30秒,可以通過 lockWactchdogTimeout 參數來改變。

    加鎖的時間默認是30秒,如果加鎖的業務沒有執行完,那麼每隔 30 ÷ 3 = 10秒,就會進行一次續期,把鎖重置成30秒,保證解鎖前鎖不會自動失效。

    那萬一業務的機器宕機了呢?如果宕機了,那看門狗線程就執行不了了,就續不了期,那自然30秒之後鎖就解開了唄。

    4、redisson分散式鎖的關鍵點:

    a. 對key不設置過期時間,由Redisson在加鎖成功後給維護一個watchdog看門狗,watchdog負責定時監聽並處理,在鎖沒有被釋放且快要過期的時候自動對鎖進行續期,保證解鎖前鎖不會自動失效

    b. 通過Lua腳本實現了加鎖和解鎖的原子操作

    c. 通過記錄獲取鎖的客戶端id,每次加鎖時判斷是否是當前客戶端已經獲得鎖,實現了可重入鎖。

    5、Redisson的使用:

    在方案三中,我們已經演示了基於Redisson的RedLock的使用案例,其實 Redisson 也封裝 可重入鎖(Reentrant Lock)、公平鎖(Fair Lock)、聯鎖(MultiLock)、紅鎖(RedLock)、讀寫鎖(ReadWriteLock)、 信號量(Semaphore)、可過期性信號量(PermitExpirableSemaphore)、 閉鎖(CountDownLatch)等,具體使用說明可以參考官方文檔:Redisson的分散式鎖和同步器

    附:redLock的官方文檔翻譯

    作者:張維鵬

    原文鏈接:
    https://blog.csdn.net/a745233700/article/details/88084219