天天看點

關于分布式鎖的面試題都在這裡了

雲栖号資訊:【 點選檢視更多行業資訊

在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

引言

為什麼要學習分布式鎖?

最簡單的理由就是作為一個社招程式員,面試的時候一定被面啦,你看怎麼多公衆号都翻來覆去的發分布式鎖的主題,可見它很重要啦,在聯考裡這就是送分題,不要怪可惜的。

那應屆生也會問嗎?這就不一定了,但是,如果你會,面試官肯定會多給你那點分(錢)

第三,分布式鎖在稍微有丢丢規模大系統裡是必備技能啦。認真看看吧。

分布式鎖要解決的問題

分布式鎖是一個在分布式環境中的重要原語,它表明不同程序間采用互斥的方式操作共享資源。常見的場景是作為一個sdk被引入到大型項目中,主要解決兩類問題:

  • 提升效率:加鎖是為了避免不必要的重複處理。例如防止幂等任務被多個執行者搶占。此時對鎖的正确性要求不高;
  • 保證正确性:加鎖是為了避免Race Condition導緻邏輯錯誤。例如直接使用分布式鎖實作防重,幂等機制。此時如果鎖出現錯誤會引起嚴重後果,是以對鎖的正确性要求高。

Java裡的鎖:

鎖是開發過程中十分常見的工具,你一定不陌生,悲觀鎖,樂觀鎖,排它鎖,公平鎖,非公平鎖等等,很多概念。但是對于初學者,知道這些鎖的概念,由于缺乏實際工作經驗,可能并不了解鎖的實際使用場景,Java中可以通過Volatile、Synchronized、ReentrantLock 三個關鍵字來實作線程的安全,這部分知識在第一輪基礎面試裡一定會問(要熟練掌握哦)。

在分布式系統中Java這些鎖技術是無法同時鎖住兩台機器上的代碼,是以要通過分布式鎖來實作,熟練使用分布式鎖也是大廠開發必會的技能。

1.面試官:你有遇到需要使用分布式鎖的場景嗎?

問題分析:

這個問題主要作為引子,先要了解什麼場景下需要使用分布式鎖,分布式鎖要解決什麼問題,在此前提下有助于你更好的了解分布式鎖的實作原理。

使用分布式鎖的場景一般需要滿足以下場景:

系統是一個分布式系統,java的鎖已經鎖不住了。操作共享資源,比如庫裡唯一的使用者資料。同步通路,即多個程序同時操作共享資源。

我:

說一個我在項目中使用分布式鎖場景的例子:

消費積分在很多系統裡都有,信用卡,電商網站,通過積分換禮品等,這裡“消費積分”這個操作典型的需要使用鎖的場景。

事件A: 以積分兌換禮品為例來講,完整的積分消費過程簡單分成3步:

A1:使用者選中商品,發起兌換送出訂單。

A2:系統讀取使用者剩餘積分:判斷使用者目前積分是否充足。

A3:扣掉使用者積分。

事件B: 系統給使用者發放積分也簡單分成3步:

B1:計算使用者當天應得積分

B2:讀取使用者原有積分

B3:在原有積分上增加本次應得積分

那麼問題來了,如果使用者消費積分和使用者累加積分同時發生(同時使用者積分進行操作)會怎樣?

假設:使用者在消費積分的同時恰好離線任務在計算積分給使用者發放積分(如根據使用者當天的消費額),這兩件事同時進行,下面的邏輯有點繞,耐心了解。

使用者U有1000積分(記錄使用者積分的資料可以了解為共享資源),本次兌換要消耗掉999積分。

不加鎖的情況:事件A程式在執行到第2步讀積分時,A:2操作讀到的結果是1000分,判斷剩餘積分夠本次兌換,緊接着要執行第3步A:3操作扣積分(1000 - 999 = 1),正常結果應該是使用者還是1分。但是這個時候事件B也在執行,本次要給使用者U發放100積分,兩個線程同時進行(同步通路),不加鎖的情況,就會有下面這種可能,A:2 -> B:2 -> A:3 -> B:3 ,在A:3尚未完成前(扣積分,1000 - 999),使用者U總積分被事件B的線程讀取了,最後使用者U的總積分變成了1100分,還白白兌換了一個999積分的禮物,這顯然不符合預期結果。

有人說怎麼可能這麼巧同時操作使用者積分,cpu那麼快,隻要使用者足夠多,并發量足夠大,墨菲定律遲早生效,出現上述bug隻是時間問題,還有可能被黑産行業卡住這個bug瘋狂薅羊毛,這個時候作為開發人員要解決這個隐患就必須了解鎖的使用。

(寫代碼是一項嚴謹的事兒!)

Java本身提供了兩種内置的鎖的實作,一種是由JVM實作的synchronized 和 JDK 提供的 Lock,以及很多原子操作類都是線程安全的,當你的應用是單機或者說單程序應用時,可以使用這兩種鎖來實作鎖。

但是當下網際網路公司的系統幾乎都是分布式的,這個時候Java自帶的 synchronized 或 Lock 已經無法滿足分布式環境下鎖的要求了,因為代碼會部署在多台機器上,為了解決這個問題,分布式鎖應運而生,分布式鎖的特點是多程序,多個實體機器上無法共享記憶體,常見的解決辦法是基于記憶體層的幹涉,落地方案就是基于Redis的分布式鎖 or ZooKeeper分布式鎖。

(我分析的不能更詳細了,面試官再不滿意?)

2.面試官:那常見的分布式鎖有哪些解決方案,你有了解嗎?

我:常見的就三種辦法吧!

Reids的分布式鎖,很多大公司會基于Reidis做擴充開發。基于Zookeeper基于資料庫,比如Mysql。

3.面試官:說說Redis分布式鎖實作方法

目前分布式鎖的實作方式主要有兩種,1.基于Redis Cluster模式。2.基于Zookeeper 叢集模式。

優先掌握這兩種,應付面試基本沒問題了。

加鎖的方式大緻有三種,分别是DB分布式鎖,Redis分布式鎖,Zookepper分布式鎖。

1.基于Redis的分布式鎖

方法一:使用setnx指令加鎖

關于分布式鎖的面試題都在這裡了

代碼解釋:

  • setnx指令,意思就是 set if not exist,如果lockKey不存在,把key存入Redis,儲存成功後如果result傳回1,表示設定成功,如果非1,表示失敗,别的線程已經設定過了。
  • expire(),設定過期時間,防止死鎖,假設,如果一個鎖set後,一直不删掉,那這個鎖相當于一直存在,産生死鎖。

    (講到這裡,我還要和面試官強調一個“但是”)

思考,我上面的方法哪裡與缺陷?繼續給面試官解釋...

加鎖總共分兩步,第一步jedis.setnx,第二步jedis.expire設定過期時間,setnx與expire不是一個原子操作,如果程式執行完第一步後異常了,第二步jedis.expire(lockKey, expireTime)沒有得到執行,相當于這個鎖沒有過期時間,有産生死鎖的可能。正對這個問題如何改進?

改進:

關于分布式鎖的面試題都在這裡了

将加鎖和設定過期時間合二為一,一行代碼搞定,原子操作。

(沒等面試官開口追問,面試官很滿意了)

面試官: 那解鎖操作呢?

釋放鎖就是删除key

使用del指令解鎖

關于分布式鎖的面試題都在這裡了

代碼解釋: 通過 requestId 判斷加鎖與解鎖是不是同一個用戶端和 jedis.del(lockKey) 兩步不是原子操作,理論上會出現在執行完第一步if判斷操作後鎖其實已經過期,并且被其它線程擷取,這是時候在執行jedis.del(lockKey)操作,相當于把别人的鎖釋放了,這是不合理的。當然,這是非常極端的情況,如果unLock方法裡第一步和第二步沒有其它業務操作,把上面的代碼扔到線上,可能也不會真的出現問題,原因第一是業務并發量不高,根本不會暴露這個缺陷,那麼問題還不大。

但是寫代碼是嚴謹的工作,能完美則必須完美。針對上述代碼中的問題,提出改進。

代碼改進:

關于分布式鎖的面試題都在這裡了

通過 jedis 用戶端的 eval 方法和 script 腳本一行代碼搞定,解決方法一中的原子問題。

4.面試官:說說基于 ZooKeeper 的分布式鎖實作原理

還是積分消費與積分累加的例子:事件A和事件B同時需要進行對積分的修改操作,兩台機器同時進行,正确的業務邏輯上讓一台機器先執行完後另外一個機器再執行,要麼事件A先執行,要麼事件B先執行,這樣才能保證不會出現A:2 -> B:2 -> A:3 -> B:3這種積分越花越多的情況(想到這種bug一旦上線,老闆要生氣了,我可能要哭了)。

怎麼辦?使用 zookeeper 分布式鎖。

一個機器接收到了請求之後,先擷取 zookeeper 上的一把分布式鎖(zk會建立一個 znode),執行操作;然後另外一個機器也嘗試去建立那個 znode,結果發現自己建立不了,因為被别人建立了,那隻能等待,等第一個機器執行完了方可拿到鎖。

使用 ZooKeeper 的順序節點特性,假如我們在/lock/目錄下建立3個節點,ZK叢集會按照發起建立的順序來建立節點,節點分别為/lock/0000000001、/lock/0000000002、/lock/0000000003,最後一位數是依次遞增的,節點名由zk來完成。

ZK中還有一種名為臨時節點的節點,臨時節點由某個用戶端建立,當用戶端與ZK叢集斷開連接配接,則該節點自動被删除。

EPHEMERAL_SEQUENTIAL為臨時順序節點。

根據ZK中節點是否存在,可以作為分布式鎖的鎖狀态,以此來實作一個分布式鎖,下面是分布式鎖的基本邏輯:

用戶端調用create()方法建立名為“/dlm-locks/lockname/lock-”的臨時順序節點。用戶端調用getChildren(“lockname”)方法來擷取所有已經建立的子節點。用戶端擷取到所有子節點path之後,如果發現自己在步驟1中建立的節點是所有節點中序号最小的,就是看自己建立的序列号是否排第一,如果是第一,那麼就認為這個用戶端獲得了鎖,在它前面沒有别的用戶端拿到鎖。如果建立的節點不是所有節點中需要最小的,那麼則監視比自己建立節點的序列号小的最大的節點,進入等待。直到下次監視的子節點變更的時候,再進行子節點的擷取,判斷是否擷取鎖。

釋放鎖的過程相對比較簡單,就是删除自己建立的那個子節點即可,不過也仍需要考慮删除節點失敗等異常情況。

5.面試官:ZK和Reids的差別,各自有什麼優缺點?

先說Reids:

Rdis隻保證最終一緻性,副本間的資料複制是異步進行(Set是寫,Get是讀,Reids叢集一般是讀寫分離架構,存在主從同步延遲情況),主從切換之後可能有部分資料沒有複制過去可能會丢失鎖情況,故強一緻性要求的業務不推薦使用Reids,推薦使用zk。Redis叢集各方法的響應時間均為最低。随着并發量和業務數量的提升其響應時間會有明顯上升(公有叢集影響因素偏大),但是極限qps可以達到最大且基本無異常

再說ZK:

使用ZooKeeper叢集,鎖原理是使用ZooKeeper的臨時節點,臨時節點的生命周期在Client與叢集的Session結束時結束。是以如果某個Client節點存在網絡問題,與ZooKeeper叢集斷開連接配接,Session逾時同樣會導緻鎖被錯誤的釋放(導緻被其他線程錯誤地持有),是以ZooKeeper也無法保證完全一緻。ZK具有較好的穩定性;響應時間抖動很小,沒有出現異常。但是随着并發量和業務數量的提升其響應時間和qps會明顯下降。

如何選擇?(僅供參考,根據我個人經驗)

關于分布式鎖的面試題都在這裡了

提示

使用分布式鎖,必須滿足兩個條件之一:

1.業務本身不要求強一緻性,可以接受偶爾出現鎖被其他線程重複擷取。

2.業務本身要求強一緻性,如果鎖被錯誤地重複擷取,必須有降級方案保證一緻性。

無論ZooKeeper與Redis,在極端情況下(例如整個ZK叢集失效,例如Reids的Master失效而Slave沒完全同步)都會存在正在被加鎖的資源被重複加鎖的問題。這種不可靠的機率極低,主要依賴于Zk叢集與Redis叢集。

6.Mysql如何做分布式鎖?

分布式鎖還可以從資料庫下手解決問題

方法一:

利用 Mysql 的鎖表,建立一張表,設定一個 UNIQUE KEY 這個 KEY 就是要鎖的 KEY,是以同一個 KEY 在mysql表裡隻能插入一次了,這樣對鎖的競争就交給了資料庫,處理同一個 KEY 資料庫保證了隻有一個節點能插入成功,其他節點都會插入失敗。

DB分布式鎖的實作:通過主鍵id的唯一性進行加鎖,說白了就是加鎖的形式是向一張表中插入一條資料,該條資料的id就是一把分布式鎖,例如當一次請求插入了一條id為1的資料,其他想要進行插入資料的并發請求必須等第一次請求執行完成後删除這條id為1的資料才能繼續插入,實作了分布式鎖的功能。

這樣 lock 和 unlock 的思路就很簡單了,僞代碼:

方法二:

使用流水号+時間戳做幂等操作,可以看作是一個不會釋放的鎖。

7.面試官:你了解業界哪些大公司的分布式鎖架構

我: 是時候展示我知識廣度的時候了~

1.Google:Chubby

Chubby是一套分布式協調系統,内部使用Paxos協調Master與Replicas。

Chubby lock service被應用在GFS, BigTable等項目中,其首要設計目标是高可靠性,而不是高性能。

Chubby被作為粗粒度鎖使用,例如被用于選主。持有鎖的時間跨度一般為小時或天,而不是秒級。

Chubby對外提供類似于檔案系統的API,在Chubby建立檔案路徑即加鎖操作。Chubby使用Delay和SequenceNumber來優化鎖機制。Delay保證用戶端異常釋放鎖時,Chubby仍認為該用戶端一直持有鎖。Sequence number 指鎖的持有者向Chubby服務端請求一個序号(包括幾個屬性),然後之後在需要使用鎖的時候将該序号一并發給 Chubby 伺服器,服務端檢查序号的合法性,包括 number 是否有效等。

2.京東SharkLock

SharkLock是基于Redis實作的分布式鎖。鎖的排他性由SETNX原語實作,使用timeout與續租機制實作鎖的強制釋放。

3.螞蟻金服SOFAJRaft-RheaKV 分布式鎖

RheaKV 是基于 SOFAJRaft 和 RocksDB 實作的嵌入式、分布式、高可用、強一緻的 KV 存儲類庫。

RheaKV對外提供lock接口,為了優化資料的讀寫,按不同的存儲類型,提供不同的鎖特性。RheaKV提供wathcdog排程器來控制鎖的自動續租機制,避免鎖在任務完成前提前釋放,和鎖永不釋放造成死鎖。

4.Netflix: Curator

Curator是ZooKeeper的用戶端封裝,其分布式鎖的實作完全由ZooKeeper完成。

在ZooKeeper建立EPHEMERAL_SEQUENTIAL節點視為加鎖,節點的EPHEMERAL特性保證了鎖持有者與ZooKeeper斷開時強制釋放鎖;節點的SEQUENTIAL特性避免了加鎖較多時的驚群效應。

總結

針對分布式鎖的兩種實作方法,使用哪種需要取決于業務場景,如果系統接口的讀寫操作完全是基于記憶體操作的,那顯然使用Redis更合适,Mysql表鎖or行鎖明顯不合适。同樣是基于記憶體的 Redis鎖 和 ZK鎖具體選用哪一種,要根據是否有具體環境和架構師對哪種技術更為了解,原則就是選你最了解到,目的是能解決問題。

【雲栖号線上課堂】每天都有産品技術專家分享!

課程位址:

https://yqh.aliyun.com/live

立即加入社群,與專家面對面,及時了解課程最新動态!

【雲栖号線上課堂 社群】

https://c.tb.cn/F3.Z8gvnK

原文釋出時間:2020-04-24

本文作者:是王炸呀

本文來自:“

掘金

”,了解相關資訊可以關注“掘金”