大廠都聊分佈式系統,面試不知道分佈式鎖如何聊下去

公衆號[JavaQ]原創,專注分享Java基礎原理分析、實戰技術、微服務架構、分佈式系統構建,誠邀點贊關注!

面試官:項目中使用過分佈式鎖嗎?

小小白:用過。

面試官:爲什麼要使用分佈式鎖?

小小白:爲了保證一個方法在高併發情況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。但是,隨着業務發展的需要,原單體單機部署的系統被演化成分佈式系統後,由於分佈式系統多線程、多進程並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,爲了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題。

面試官:項目中用到的分佈式鎖是你自己實現的,還是別人寫的?

小小白:公司中間件部門二次開發的。

面試官:有沒有研究過它的具體實現?

小小白:它是在Redisson的基礎上再次封裝的,因爲Redisson已經實現了一套完整的分佈式鎖解決方案,所以只要做簡單的封裝就是可以很輕鬆的使用。

面試官:有沒有了解過Redisson實現的分佈式鎖原理?

小小白:使用key來作爲是否上鎖的標誌,當通過getLock(String key)方法獲得相應的鎖之後,這個key即作爲一個鎖存儲到Redis集羣中,在接下來如果有其他的線程嘗試獲取名爲key的鎖時,便會向集羣中進行查詢,如果能夠查到這個鎖並發現相應的value的值不爲0,則表示已經有其他線程申請了這個鎖同時還沒有釋放,則當前線程進入阻塞,否則由當前線程獲取這個鎖並將value值加一,如果是可重入鎖的話,則當前線程每獲得一個自身線程的鎖,就將value的值加一,而每釋放一個鎖則將value值減一,直到減至0,完全釋放這個鎖。底層通過eval命令來執行Lua腳本,保證複雜業務邏輯執行的原子性。

面試官:如果讓你實現一個分佈式鎖,你會有哪些實現方案?

小小白:這個之前有了解過,基於數據庫的實現方式、基於Redis的實現方式和基於ZooKeeper的實現方式。

面試官:使用數據庫如何實現?

小小白:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。

面試官:據我瞭解這種實現方案基本沒人使用,爲什麼?

小小白:這種實現方式很簡單,但是對於分佈式鎖應該具備的條件來說,它有一些問題需要解決:

  • 因爲是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;

  • 不具備可重入的特性,因爲同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;

  • 沒有鎖失效機制,因爲有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;

  • 不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。

面試官:使用Redis如何實現分佈式鎖?

小小白:在Redis2.6.12版本之前,使用setnx命令設置key-value、使用expire命令設置key的過期時間獲取分佈式鎖,使用del命令釋放分佈式鎖,但這種實現方式會出現死鎖、誤刪持有的鎖、主從機制數據不同步的問題。所以,從Redis2.6.12版本開始,通過SET resource_name my_random_value NX PX max-lock-time來實現分佈式鎖,這個命令僅在不存在key(resource_name)的時候才能被執行成功(NX選項),並且這個key有一個max-lock-time秒的自動失效時間(PX屬性)。這個key的值是“my_random_value”,它是一個隨機值,這個值在所有的機器中必須是唯一的,用於安全釋放鎖。同時,釋放鎖的時候,只有key存在並且存儲的“my_random_value”值和指定的值一樣才執行del命令,此過程通過Lua腳本執行,保證原子性。而且,不採用主從複製機制,使用RedLock算法解決獲取鎖和釋放鎖的單點故障問題。

面試官:你剛剛說到RedLock算法,它的原理是什麼?

小小白:在Redis的分佈式環境中,假設有5個Redis master,這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制。爲了取到鎖,客戶端應該執行以下操作:

  1. 獲取當前Unix時間,以毫秒爲單位;

  2. 依次嘗試從N個實例,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試另外一個Redis實例;

  3. 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(這裏是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。

  4. 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果);

  5. 如果因爲某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功)。

面試官:你再說一下基於ZooKeeper的實現方式?

小小白:基於ZooKeeper實現分佈式鎖的步驟如下:

  1. 創建一個目錄mylock;

  2. 線程A想獲取鎖就在mylock目錄下創建臨時順序節點;

  3. 獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;

  4. 線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;

  5. 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

推薦一個apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

面試官:基於ZooKeeper的實現方式有什麼優缺點?

小小白:高可用、可重入、阻塞鎖特性,可解決失效死鎖問題,但是因爲需要頻繁的創建和刪除節點,性能上不如Redis方式。

往期推薦

面試官:SpringBoot中關於日誌工具的使用,我想問你幾個常見問題面試被問爲什麼使用Spring Boot?答案好像沒那麼簡單

面試官:Spring框架內置了哪些可擴展接口,咱們一個一個聊

Spring聲明式事務處理的實現原理,來自面試官的窮追拷問

Spring MVC相關面試題就是無底洞,反正我是怕了

說實話,面試這麼問Spring框架的問題,我快扛不住了

沒使用加號拼接字符串,面試官竟然問我爲什麼

面試官一步一步的套路你,爲什麼SimpleDateFormat不是線程安全的

都說ThreadLocal被面試官問爛了,可爲什麼面試官還是喜歡繼續問

Java註解是如何玩轉的,面試官和我聊了半個小時

如何去除代碼中的多次if而引發的一連串面試問題

String引發的提問,我差點跪了

就寫了一行代碼,被問了這麼多問題

面試官:JVM對鎖進行了優化,都優化了啥?

synchronized連環問

支持原創,我點【在看 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章