Java分散式鎖那點事
為什麼要使用分散式鎖
為了保證一個方法在高併發情況下的同一時間只能被同一個執行緒執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。
分散式鎖的三種實現方式
在分析分散式鎖的三種實現方式之前,
在分散式系統環境下,一個方法在同一時間只能被一個機器的一個執行緒執行;
高可用的獲取鎖與釋放鎖;
高性能的獲取鎖與釋放鎖;
具備可重入特性;
具備鎖失效機制,防止鎖死;
具備阻塞鎖特性,即沒有獲取到鎖將繼續等待獲取鎖;
具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。
基於資料庫的實現方式
在資料庫中創建一個表,表中包含方法名等欄位,並在方法名欄位上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入資料,成功插入則獲取鎖,執行完成後刪除對應的行資料釋放鎖。
這種實現方式很簡單,但是對於分散式鎖應該具備的條件來說,它有一些問題需要解決及優化。
因為是基於資料庫實現的,資料庫的可用性和性能將直接影響分散式鎖的可用性及性能,
不具備可重入的特性,因為同一個執行緒在釋放鎖之前,行資料一直存在,無法再次成功插入資料,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和執行緒資訊,在再次獲取鎖的時候,先查詢表中機器和執行緒資訊是否和當前機器和執行緒相同,若相同則直接獲取鎖;
沒有鎖失效機制,
不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,迴圈多次去獲取。
優點:借助資料庫,方案簡單。
缺點:在實際實施的過程中會遇到各種不同的問題,為了解決這些問題,
基於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方式。
總結
上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。
如果你想學習Java工程化、高性能及分散式、深入淺出。性能調優、Spring,MyBatis,Netty源碼分析等知識點可以來找我。
而現在我就有一個平臺可以提供給你們學習,讓你在實踐中積累經驗掌握原理。主要方向是JAVA架構師。如果你想拿高薪,想突破瓶頸,想跟別人競爭能取得優勢的,想進BAT但是有擔心面試不過的,可以加我的Java架構進階群:675047716
注:加群要求
1、具有2-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。
3、如果沒有工作經驗,但基礎非常扎實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。
5.阿裡Java高級大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!
6.小號加群一律不給過,謝謝。
轉發此文章請帶上原文連結,否則將追究法律責任!
還沒來得及使用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方式。
總結
上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。
如果你想學習Java工程化、高性能及分散式、深入淺出。性能調優、Spring,MyBatis,Netty源碼分析等知識點可以來找我。
而現在我就有一個平臺可以提供給你們學習,讓你在實踐中積累經驗掌握原理。主要方向是JAVA架構師。如果你想拿高薪,想突破瓶頸,想跟別人競爭能取得優勢的,想進BAT但是有擔心面試不過的,可以加我的Java架構進階群:675047716
注:加群要求
1、具有2-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。
3、如果沒有工作經驗,但基礎非常扎實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。
5.阿裡Java高級大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!
6.小號加群一律不給過,謝謝。
轉發此文章請帶上原文連結,否則將追究法律責任!