03.02 如何正確使用redis分佈式鎖?

點關注,不迷路;持續更新Java相關技術及資訊!!!

筆者在公司擔任技術面試官,在筆者面試過程中,如果面試候選人提到了reids分佈式鎖,筆者都會問一下redis分佈式鎖的知識點,但是令筆者遺憾的是,該知識點十個人中有九個人都答得不清楚,或者回答錯誤,這讓筆者有了寫這篇文章的想法,來幫助童鞋們正確認識reids分佈式鎖.

什麼是分佈式鎖?為什麼需要分佈式鎖?

  在java中,在單進程多線程的情況下,為了防止多個線程共同競爭同一個資源,因此需要鎖,java中有顯示鎖和隱式鎖來保證,而在多進程的情況下,普通的鎖就無法滿足要求了,因此我們需要分佈式鎖,常用的分佈式鎖解決方案有三種,分別是基於數據庫/redis/zookeeper,本文我們主要討論redis分佈式鎖.

redis分佈式鎖實現

  筆者在面試過程中,問redis分佈式鎖知識點時的第一個問題就是如何實現一個redis分佈式鎖,許多候選人直接說,啊,這很簡單啊,使用setNx()方法,設置一個過期時間就可以了,我接著問,那如何釋放鎖鎖呢?候選人回答說,那很簡單啊,直接調用delete方法就可以了,我接著問,釋放鎖直接調用delete方法就可以了嗎?候選人回答,對啊,delete方法也是線程安全的,我.....那麼如何實現一個redis分佈式鎖,我們用代碼來演示一下,首先來看一下加鎖的代碼片段:

如何正確使用redis分佈式鎖?

  首先我們的分佈式鎖實現了Lock接口,然後主要看我們的lock方法,阻塞式自旋鎖,加鎖方法直接使用tryLock(),接下來我們再來看看tryLock()方法:

如何正確使用redis分佈式鎖?

  其實很簡單,主要是調用redis的set方法(其中UUID筆者為了方便演示,直接使用UUID,在分佈式的生產環境下應該使用諸如雪花算法等來保證分佈式系統下UUDI的唯一性),如果返回OK則說明加鎖成功,否則失敗,再來看看釋放鎖的方法,面試過程中很多候選人童鞋說直接調用delete方法,我們寫一段代碼,然後分析一下直接調用delete方法的問題:

如何正確使用redis分佈式鎖?

  如上一段代碼,假設一種極端場景下有兩個線程A和B,A線程先獲取鎖,設置過期時間為10秒,然後A線程執行釋放鎖操作,執行到if判斷語句並且成功進入時,此時耗時剛好10秒,鎖過期了,並且CPU分配給A線程的時間片剛好用完,此時B線程開始執行並且成功獲取到該分佈式鎖,然後執行一段時間後B線程的時間片用完,此時A繼續執行刪除操作,此時A刪除的就是B線程的鎖,會造成誤刪除操作,因此為了避免這種情況,我們需要一種機制來保證判斷和刪除操作的原子性,redis官方推薦我們使用lua腳本,因此正確的解鎖方式如下:

如何正確使用redis分佈式鎖?

  其中的UNLOCKLUASCRIPT如下:

如何正確使用redis分佈式鎖?

  redis使用lua腳本能保證該操作的原子性,因此這樣才能正確釋放分佈式鎖.這也回答了為什麼之前說釋放鎖的時候直接調用delete方法是錯誤的.

有什麼問題?

  在上文中,我們使用redis構建了一個分佈式鎖,但是請注意,該代碼在單機環境下沒有任何問題,但是我們在生產中往往都是redis集群部署,由於redis主從節點的數據同步是異步的,如果Redis的master節點在鎖未同步到Slave節點的時候宕機了怎麼辦?

舉例來說:  

1.進程A在master節點獲得了鎖。  

2.在鎖同步到slave之前,master宕機,數據還沒有同步到slave  

3.slave變成了新的master節點  

4.進程B也得到了和A相同的鎖.  

因此,如果你的業務允許在master宕機期間,多個客戶端允許同時都持有鎖,那如上的分佈式鎖是可以接受的,否則就不能使用上述的分佈式鎖,在這種情況下

redis官方為我們提供了另一種解決方案----RedLock算法.

RedLock算法

  假設我們有N個Master節點(N一般為奇數),這些節點互相之間相互獨立,不需要進行數據同步,我們用在單節點獲取和釋放鎖的方式來操縱這些節點,具體過程為:  

1.獲取當前時間(單位是毫秒)。  

2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。  

3.客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(N/2+1在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。  

4.如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。  

5.如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。  

有關RedLock的算法,可以詳見官網文檔

RedLock算法是否真的足夠安全?

RedLock仍然是不安全的,簡單來說,RedLock最大的弊端有兩個:  

1.進程由於各種原因pause,類似於上文說的多線程間的時間片切換,比如由於GC停頓等導致鎖過期,但是進程並未感知到,同時另一個進程已經獲取了該分佈式鎖,就會導致奇怪的結果發生.  

2.算法對時鐘依賴性太強,假設N個節點為5,按照超過一半的原則,假設進程X成功獲取了A/B/C三個節點的鎖,此時認為X獲取鎖成功,此時X在TTL時間段內沒有執行完成,鎖到期自動釋放,此時由於C節點的時間比A/B節點快,導致C節點先釋放鎖,此時Y節點獲取了C/D/E三個節點的鎖,又導致兩個進程獲取了同一個鎖.  

無論如何,在多master節點的情況下,沒有任何方案能完美保證RedLock的絕對安全,因此,我們在使用redis分佈式鎖的時候一定要弄清楚我們的目的是什麼?

一般來說,有兩種情況:  

1.為了提高性能.比如持有該鎖使我們的程序不會進行重複的計算,在這種情況下,如果鎖失敗了我們付出的代價僅僅是進行了重複的計算,不會影響我們的業務結果.  

2.為了保證業務的正確性.比如我們是一個銀行系統,為了保證轉賬操作扣款唯一性,擁有該鎖可以確保我們的扣款操作的唯一性,如果鎖失效,會導致多次扣款,這是無法接受的.

  如果我們是為了提升性能,那沒有必要使用RedLock算法,它成本高(假設需要5個master節點,這些節點還要保證高可用,則需要更多的節點)且又複雜,不如使用在單機情況下的分佈式鎖,前提是你的業務能容忍我們上述說的宕機期間相同鎖的問題.  

如果是為了保證業務的正確性,我們說了RedLock也不能完美保證絕對安全,因此也不能放心的使用RedLock.

總結

  總而言之,使用Redis分佈式鎖實在不是一個好的選擇,Redis設計的初衷也並不是滿足分佈式鎖的需求.對於需求性能的分佈式鎖應用它太重了且成本高;對於需求正確性的應用來說它不夠安全.如果你的應用只需要高性能的分佈式鎖並且不要求多高的正確性,那麼單節點的Redis分佈式鎖足夠了;如果你的應用想要保證正確性,那麼不建議 RedLock,建議使用一個合適的一致性協調系統,比如基於Zookeeper的分佈式鎖!

<code>2020年最新源碼分析課程資料,關注私信【555】獲取,還可領取更多Java面試題資料和Java架構學習資料/<code> 


分享到:


相關文章: