機房建置,目前幾乎很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的蓋理論告訴我們“任何一個分佈式系統都無法同時滿足一致性(一致性),可用性(可用性)和分區容錯性(分區容忍),最多隻能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證”最終一致性”,只要這個最終時間是在用戶可以接受的範圍內即可。
在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務,分佈式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了很多併發處理相關的API,但是這些API在分佈式場景中就無能爲力了。也就是說單純的Java API並不能提供分佈式鎖的能力,所以針對分佈式鎖的實現目前有多種方案。
針對分佈式鎖的實現,目前比較常用的有以下幾種方案:基於數據庫實現分佈式鎖基於緩存實現分佈式鎖基於管理員實現分佈式鎖在分析這幾種實現方案之前我們先來想一下,我們需要的分佈式鎖應該是怎麼樣的嗎?(這裏以方法鎖爲例,資源鎖同理)
可以保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。
這把鎖要是一把可重入鎖(避免死鎖)
這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
有高可用的獲取鎖和釋放鎖功能獲取鎖和釋放鎖的性能要好基於數據庫實現分佈式鎖基於數據庫表要實現分佈式鎖,最簡單的方式可能就是直接創建一張鎖表,然後通過操作該表中的數據來實現了。
當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
創建這樣一張數據庫表:創建表“methodLock”(“id”int(11)NOT NULL AUTO_INCREMENT評論“主鍵”,“method_name”varchar(64)NOT NULL默認”評論“鎖定的方法名’,‘desc varchar(1024)NOT NULL默認“備註信息”、“update_time”時間戳NOT NULL違約CURRENT_TIMESTAMP更新CURRENT_TIMESTAMP評論“保存數據時間,自動生成的,主鍵(id),唯一鍵“uidx_method_name”(“method_name”)使用BTREE)引擎= InnoDB默認字符集= utf8評論=“鎖定中的方法”;當我們想要鎖住某個方法時,執行以下SQL:插入methodLock(method_name desc)值(“method_name”、“desc”)
因爲我們對method_name做了唯一性約束,這裏如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那麼我們就可以認爲操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。
當方法執行完畢之後,想要釋放鎖的話,需要執行以下Sql:刪除從methodLock method_name =“method_name”上面這種簡單的實現有以下幾個問題:1,這把鎖強依賴數據庫的可用性、數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。
2,這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
3,這把鎖只能是非阻塞的,因爲數據的插入操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。
4,這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。
當然,我們也可以有其他方式解決上面的問題。
數據庫是單點?搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。
沒有失效時間?只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍
非阻塞的?搞一個而循環,直到插入成功再返回成功。
非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。
基於數據庫排他鎖除了可以通過增刪操作數據表中的記錄以外,其實還可以藉助數據中自帶的鎖來實現分佈式的鎖。
我們還用剛剛創建的那張數據庫表。可以通過數據庫的排他鎖來實現分佈式鎖。基於MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:公共邏輯鎖(){ connection.setAutoCommit(假)
而(真){嘗試{結果= select *從methodLock method_name = xxx更新;如果(結果= = null){返回true;} }捕捉(異常e){
}睡眠(1000);}返回false;}在查詢語句後面增加更新,數據庫會在查詢過程中給數據庫表增加排他鎖(這裏再多提一句,InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候纔會使用行級鎖,否則會使用表級鎖。這裏我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要創建成唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上)。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖。
我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖,當獲取到鎖之後,可以執行方法的業務邏輯,執行完方法之後,再通過以下方法解鎖:公共空間解鎖(){ connection.commit();}通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖?更新語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。
鎖定之後服務宕機,無法釋放?使用這種方式,服務宕機之後數據庫會自己把鎖釋放掉。
但是還是無法直接解決數據庫單點和可重入問題。
這裏還可能存在另外一個問題,雖然我們對method_name使用了唯一索引,並且顯示使用更新來使用行級鎖。但是,MySql會對查詢進行優化,即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySql通過判斷不同執行計劃的代價來決定的,如果MySql認爲全表掃效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。如果發生這種情況就悲劇了……
還有一個問題,就是我們要使用排他鎖來進行分佈式鎖的鎖,那麼一個排他鎖長時間不提交,就會佔用數據庫連接,一旦類似的連接變得多了,就可能把數據庫連接池撐爆總結總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數據庫的排他鎖來實現分佈式鎖。
數據庫實現分佈式鎖的優點直接藉助數據庫,容易理解。
數據庫實現分佈式鎖的缺點會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。
操作數據庫需要一定的開銷,性能問題需要考慮。
使用數據庫的行級鎖並不一定靠譜,尤其是當我們的鎖表並不大的時候。
基於緩存實現分佈式鎖相比較於基於數據庫實現分佈式鎖的方案來說,基於緩存來實現在性能方面會表現的更好一點。而且很多緩存是可以集羣部署的,可以解決單點問題。
目前有很多成熟的緩存產品,包括複述,memcached以及我們公司內部的Tair。這裏以Tair爲例來分析下使用緩存實現分佈式鎖的方案。關於複述和memcached在網絡上有很多相關的文章,並且也有一些成熟的框架及算法可以直接使用。
基於Tair的實現分佈式鎖其實和複述,類似,其中主要的實現方式是使用TairManager.put方法來實現。
公共布爾trylock(String鍵){ = ldbTairManager ResultCode代碼。put(名稱空間,鑰匙,“這是一個鎖”。2 0);如果(ResultCode.SUCCESS.equals(代碼)
返回true,否則返回false;公共布爾解鎖(String鍵)} { ldbTairManager。無效(名稱空間,關鍵);}以上實現方式同樣存在幾個問題:1,這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在tair中,其他線程無法再獲得到鎖。
2,這把鎖只能是非阻塞的,無論成功還是失敗都直接返回。
3,這把鎖是非重入的,一個線程獲得鎖之後,在釋放鎖之前,無法再次獲得該鎖,因爲使用到的關鍵在tair中已經存在。無法再執行把操作。
當然,同樣有方式可以解決。
沒有失效時間? tair的把方法支持傳入失效時間,到達時間之後數據會自動刪除。
非阻塞?而重複執行。
非可重入嗎?在一個線程獲取到鎖之後,把當前主機信息和線程信息保存起來,下次再獲取之前先檢查自己是不是當前鎖的擁有者。
但是,失效時間我設置多長時間爲好?如何設置的失效時間太短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。如果設置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數據庫實現分佈式鎖同樣存在總結可以使用緩存來代替數據庫來實現分佈式鎖,這個可以提供更好的性能,同時,很多緩存服務都是集羣部署的,可以避免單點問題,並且很多緩存服務都提供了可以用來實現分佈式鎖的方法,比如Tair的把方法,複述的setnx方法等,並且,這些緩存服務也都提供了對數據的過期自動刪除的支持,可以直接設置超時時間來控制鎖的釋放。
使用緩存實現分佈式鎖的優點性能好,實現起來較爲方便。
使用緩存實現分佈式鎖的缺點通過超時時間來控制鎖的失效時間並不是十分的靠譜。
基於管理員實現分佈式鎖基於管理員臨時有序節點可以實現的分佈式鎖。
大致思想即爲:每個客戶端對某個方法加鎖時,在動物園管理員上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
沒有留言:
張貼留言