爲何要使用分佈式鎖
爲了保證一個方法在高併發狀況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的狀況下,可使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。可是,隨着業務發展的須要,原單體單機部署的系統被演化成分佈式系統後,因爲分佈式系統多線程、多進程而且分佈在不一樣機器上,這將使原單機部署狀況下的併發控制鎖策略失效,爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題。
分佈式鎖的三種實現方式
在分析分佈式鎖的三種實現方式以前,先了解一下分佈式鎖應該具有哪些條件。
- 在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
- 高可用的獲取鎖與釋放鎖;
- 高性能的獲取鎖與釋放鎖;
- 具有可重入特性;
- 具有鎖失效機制,防止死鎖;
- 具有阻塞鎖特性,即沒有獲取到鎖將繼續等待獲取鎖;
- 具有非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。
基於數據庫的實現方式
在數據庫中建立一個表,表中包含方法名等字段,並在方法名字段上建立惟一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。
這種實現方式很簡單,可是對於分佈式鎖應該具有的條件來講,它有一些問題須要解決及優化。redis
- 由於是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,因此,數據庫須要雙機部署、數據同步、主備切換;
- 不具有可重入的特性,由於同一個線程在釋放鎖以前,行數據一直存在,沒法再次成功插入數據,因此,須要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
- 沒有鎖失效機制,由於有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,因此,須要在表中新增一列,用於記錄失效時間,而且須要有定時任務清除這些失效的數據;
- 不具有阻塞鎖特性,獲取不到鎖直接返回失敗,因此須要優化獲取邏輯,循環屢次去獲取。
優勢:藉助數據庫,方案簡單。
缺點:在實際實施的過程當中會遇到各類不一樣的問題,爲了解決這些問題,實現方式將會愈來愈複雜;依賴數據庫須要必定的資源開銷,性能問題須要考慮。
基於Redis的實現方式
在Redis2.6.12版本以前,使用setnx命令設置key-value、使用expire命令設置key的過時時間獲取分佈式鎖,使用del命令釋放分佈式鎖,可是這種實現有以下一些問題:
- setnx命令設置完key-value後,還沒來得及使用expire命令設置過時時間,當前線程掛掉了,會致使當前線程設置的key一直有效,後續線程沒法正常經過setnx獲取鎖,形成死鎖;
- 在分佈式環境下,線程A經過這種實現方式獲取到了鎖,可是在獲取到鎖以後,執行被阻塞了,致使該鎖失效,此時線程B獲取到該鎖,以後線程A恢復執行,執行完成後釋放該鎖,直接使用del命令,將會把線程B的鎖也釋放掉,而此時線程B還沒執行完,將會致使不可預知的問題;
- 爲了實現高可用,將會選擇主從複製機制,可是主從複製機制是異步的,會出現數據不一樣步的問題,可能致使多個機器的多個線程獲取到同一個鎖。
針對上面這些問題,有以下一些解決方案:
- 第一個問題是由於兩個命令是分開執行而且不具有原子特性,若是能將這兩個命令合二爲一就能夠解決問題了。在Redis2.6.12版本中實現了這個功能,Redis爲set命令增長了一系列選項,能夠經過SET resource_name my_random_value NX PX max-lock-time來獲取分佈式鎖,這個命令僅在不存在key(resource_name)的時候才能被執行成功(NX選項),而且這個key有一個max-lock-time秒的自動失效時間(PX屬性)。這個key的值是「my_random_value」,它是一個隨機值,這個值在全部的機器中必須是惟一的,用於安全釋放鎖。
- 爲了解決第二個問題,用到了「my_random_value」,釋放鎖的時候,只有key存在而且存儲的「my_random_value」值和指定的值同樣才執行del命令,此過程能夠經過如下Lua腳本實現:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end複製代碼
- 第三個問題是由於採用了主從複製致使的,解決方案是不採用主從複製,使用RedLock算法,這裏引用網上一段關於RedLock算法的描述。
在Redis的分佈式環境中,假設有5個Redis master,這些節點徹底互相獨立,不存在主從複製或者其餘集羣協調機制。爲了取到鎖,客戶端應該執行如下操做:
- 獲取當前Unix時間,以毫秒爲單位;
- 依次嘗試從N個實例,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設置鎖時,客戶端應該設置一個網絡鏈接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣能夠避免服務器端Redis已經掛掉的狀況下,客戶端還在死死地等待響應結果。若是服務器端沒有在規定時間內響應,客戶端應該儘快嘗試另一個Redis實例;
- 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就獲得獲取鎖使用的時間。當且僅當從大多數(這裏是3個節點)的Redis節點都取到鎖,而且使用的時間小於鎖失效時間時,鎖纔算獲取成功。
- 若是取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果);
- 若是由於某些緣由,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在全部的Redis實例上進行解鎖(即使某些Redis實例根本就沒有加鎖成功)。
經過上面的解決方案能夠實現一個高效、高可用的分佈式鎖,這裏推薦一個成熟、開源的分佈式鎖實現,即Redisson。
優勢:高性能,藉助Redis實現比較方便。
缺點:線程獲取鎖後,若是處理時間過長會致使鎖超時失效,因此,經過鎖超時機制不是十分可靠。
基於ZooKeeper的實現方式
ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個惟一文件名。基於ZooKeeper實現分佈式鎖的步驟以下:
- 建立一個目錄mylock;
- 線程A想獲取鎖就在mylock目錄下建立臨時順序節點;
- 獲取mylock目錄下全部的子節點,而後獲取比本身小的兄弟節點,若是不存在,則說明當前線程順序號最小,得到鎖;
- 線程B獲取全部節點,判斷本身不是最小節點,設置監聽比本身次小的節點;
- 線程A處理完,刪除本身的節點,線程B監聽到變動事件,判斷本身是否是最小的節點,若是是則得到鎖。
這裏推薦一個apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優勢:具有高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:由於須要頻繁的建立和刪除節點,性能上不如Redis方式。
總結
上面的三種實現方式,沒有在全部場合都是完美的,因此,應根據不一樣的應用場景選擇最適合的實現方式。