Java分散式鎖,搞懂分散式鎖實現看這篇文章就對了

来源:https://www.cnblogs.com/toutou/archive/2018/09/24/java_lock.html
-Advertisement-
Play Games

隨著微處理機技術的發展,人們只需花幾百美元就能買到一個CPU晶元,這個晶元每秒鐘執行的指令比80年代最大的大型機的處理機每秒鐘所執行的指令還多。如果你願意付出兩倍的價錢,將得到同樣的CPU,但它卻以更高的時鐘速率運行。因此,最節約成本的辦法通常是在一個系統中使用集中在一起的大量的廉價CPU。所以,傾... ...


隨著微處理機技術的發展,人們只需花幾百美元就能買到一個CPU晶元,這個晶元每秒鐘執行的指令比80年代最大的大型機的處理機每秒鐘所執行的指令還多。如果你願意付出兩倍的價錢,將得到同樣的CPU,但它卻以更高的時鐘速率運行。因此,最節約成本的辦法通常是在一個系統中使用集中在一起的大量的廉價CPU。所以,傾向於分散式系統的主要原因是它可以潛在地得到比單個的大型集中式系統好得多的性價比。實際上,分散式系統是通過較低廉的價格來實現相似的性能的。

隨著互聯網的興起,越來越多的人使用者互聯網產品。一般互聯網系統都是分散式部署的,分散式部署確實能帶來性能和效率上的提升,提升效率的同事,我們還需要註意,保證一個分散式環境下數據一致性的問題。

v分散式鎖簡述

在單機時代,雖然不存在分散式鎖,但也會面臨資源互斥的情況,只不過在單機的情況下,如果有多個線程要同時訪問某個共用資源的時候,我們可以採用線程間加鎖的機制,即當某個線程獲取到這個資源後,就需要對這個資源進行加鎖,當使用完資源之後,再解鎖,其它線程就可以接著使用了。例如,在JAVA中,甚至專門提供了一些處理鎖機制的一些API(synchronize/Lock等)。

但是到了分散式系統的時代,這種線程之間的鎖機制,就沒作用了,系統可能會有多份並且部署在不同的機器上,這些資源已經不是線上程之間共用了,而是屬於進程之間共用的資源。因此,為瞭解決這個問題,「分散式鎖」就強勢登場了。

分散式鎖是控制分散式系統之間同步訪問共用資源的一種方式。在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共用了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分散式鎖。

在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共用了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,這個時候,便需要使用到分散式鎖。

分散式鎖要滿足哪些要求呢?

  • 排他性:在同一時間只會有一個客戶端能獲取到鎖,其它客戶端無法同時獲取
  • 避免死鎖:這把鎖在一段有限的時間之後,一定會被釋放(正常釋放或異常釋放)
  • 高可用:獲取或釋放鎖的機制必須高可用且性能佳
  • ...

目前相對主流的有三種,從實現的複雜度上來看,從上往下難度依次增加:

  • 資料庫(MySQL)
  • Redis
  • ZooKeeper

v基於資料庫實現

基於資料庫來做分散式鎖的話,通常有兩種做法:

  • 基於資料庫的樂觀鎖
  • 基於資料庫的悲觀鎖

樂觀鎖

樂觀鎖的特點先進行業務操作,不到萬不得已不去拿鎖。即“樂觀”的認為拿鎖多半是會成功的,因此在進行完業務操作需要實際更新數據的最後一步再去拿一下鎖就好。

樂觀鎖機制其實就是在資料庫表中引入一個版本號(version)欄位來實現的。當我們要從資料庫中讀取數據的時候,同時把這個version欄位也讀出來,如果要對讀出來的數據進行更新後寫回資料庫,則需要將version加1,同時將新的數據與新的version更新到數據表中,且必須在更新的時候同時檢查目前資料庫里version值是不是之前的那個version,如果是,則正常更新。如果不是,則更新失敗,說明在這個過程中有其它的進程去更新過數據了。

看圖敘事。模擬實戰場景。

請叫我頭頭哥

 

如上圖,故事男主人公(以下簡稱男主)打算去ATM機取3000元,故事女主人公(以下簡稱女主)則要在某寶買買買,買個包需要3000元,賬戶的餘額是5000元。如果沒有採用鎖的話,在兩人同時取款和買買買,可能會出現合計消費了6000,導致賬戶餘額異常。所以需要用到鎖的機制,當男主女主甚至更多小主同時消費時,除了讀取到6000的賬戶餘額外,還需要讀取到當前的版本號version=1,等先行消費成功的主人公(無論誰先消費)去出發修改賬戶餘額的同時,會觸發version=version+1,即version=2。那麼其他人使用未更新的version(1)去更新賬戶餘額時就會發現版本號不對,就會導致本次更新失敗,就得重新去讀取最新賬戶餘額以及版本號。

樂觀鎖遵循的兩點法則:

  • 鎖服務要有遞增的版本號version
  • 每次更新數據的時候都必須先判斷版本號對不對,然後再寫入新的版本號

悲觀鎖

悲觀鎖的特點是先獲取鎖,再進行業務操作,即“悲觀”的認為獲取鎖是非常有可能失敗的,因此要先確保獲取鎖成功再進行業務操作。

通常所說的“一鎖二查三更新”即指的是使用悲觀鎖。通常來講在資料庫上的悲觀鎖需要資料庫本身提供支持,即通過常用的select ... for update操作來實現悲觀鎖。當資料庫執行select for update時會獲取被select中的數據行的行鎖,因此其他併發執行的select for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。

示例:

/**
     * 消費以後更新銀行餘額
     * @param bankId 銀行卡號
     * @param cost 消費金額
     * @return
     */
    public boolean consume(Long bankId, Integer cost){
        //先鎖定銀行賬戶
        BankAccount product = query("SELECT * FROM bank_account WHERE bank_id=#{bankId} FOR UPDATE", bankId);
        if (product.getNumber() > 0) {
            int updateCnt = update("UPDATE tb_product_stock SET number=#{cost} WHERE product_id=#{productId}", cost, bankId);
            if(updateCnt > 0){    //更新庫存成功
                return true;
            }
        }
        return false;
    }

樂觀鎖與悲觀鎖的區別

樂觀鎖的思路一般是表中增加版本欄位,更新時where語句中增加版本的判斷,算是一種CAS(Compare And Swep)操作,銀行消費場景中version起到了版本控制的作用( AND version=#{version})。

悲觀鎖之所以是悲觀,在於他認為本次操作會發生併發衝突,所以一開始就對銀行賬戶加上鎖(SELECT ... FOR UPDATE),然後就可以安心的做判斷和更新,因為這時候不會有別人更新賬戶餘額。

v基於Redis實現

基於Redis實現的鎖機制,主要是依賴redis自身的原子操作,例如:

SET user_key user_value NX PX 100

redis從2.6.12版本開始,SET命令才支持這些參數:

NX:只在在鍵不存在時,才對鍵進行設置操作,SET key value NX 效果等同於 SETNX key value

PX millisecond:設置鍵的過期時間為millisecond毫秒,當超過這個時間後,設置的鍵會自動失效

上述代碼示例是指,當redis中不存在user_key這個鍵的時候,才會去設置一個user_key鍵,並且給這個鍵的值設置為 user_value,且這個鍵的存活時間為100ms

為什麼這個命令可以幫我們實現鎖機制呢?

因為這個命令是只有在某個key不存在的時候,才會執行成功。那麼當多個進程同時併發的去設置同一個key的時候,就永遠只會有一個進程成功。當某個進程設置成功之後,就可以去執行業務邏輯了,等業務邏輯執行完畢之後,再去進行解鎖。

解鎖很簡單,只需要刪除這個key就可以了,不過刪除之前需要判斷,這個key對應的value是當初自己設置的那個。

另外,針對redis集群模式的分散式鎖,可以採用redis的Redlock(可能會被牆)機制。

v基於ZooKeeper實現

其實基於ZooKeeper,就是使用它的臨時有序節點來實現的分散式鎖。

原理

當某客戶端要進行邏輯的加鎖時,就在zookeeper上的某個指定節點的目錄下,去生成一個唯一的臨時有序節點, 然後判斷自己是否是這些有序節點中序號最小的一個,如果是,則算是獲取了鎖。如果不是,則說明沒有獲取到鎖,那麼就需要在序列中找到比自己小的那個節點,並對其調用exist()方法,對其註冊事件監聽,當監聽到這個節點被刪除了,那就再去判斷一次自己當初創建的節點是否變成了序列中最小的。如果是,則獲取鎖,如果不是,則重覆上述步驟。

當釋放鎖的時候,只需將這個臨時節點刪除即可。

請叫我頭頭哥

 

如上圖,locker是一個持久節點,node_1/node_2/.../node_n 就是上面說的臨時節點,由客戶端client去創建的。

client_1/client_2/.../clien_n 都是想去獲取鎖的客戶端。以client_1為例,它想去獲取分散式鎖,則需要跑到locker下麵去創建臨時節點(假如是node_1)創建完畢後,看一下自己的節點序號是否是locker下麵最小的,如果是,則獲取了鎖。如果不是,則去找到比自己小的那個節點(假如是node_2),找到後,就監聽node_2,直到node_2被刪除,那麼就開始再次判斷自己的node_1是不是序列中最小的,如果是,則獲取鎖,如果還不是,則繼續找一下一個節點。

v博客總結

分散式鎖有很多種,開篇說的"相對主流的有三種"只是針對我所遇到的。分散式鎖未來肯定是千變萬化的,無論你身處一個什麼樣的公司,最開始的工作可能都得儘可能的從簡單的做起。希望大家能根據所在公司業務場景,選擇適合所在項目的方案。

其他參考資料:

 


作  者:請叫我頭頭哥
出  處:http://www.cnblogs.com/toutou/
關於作者:專註於基礎平臺的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回覆。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角推薦一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java的基礎性數據類型並不算多,基本類型的包裝類以及String BigInteger BigDecimal等,這是平時經常用到的,雖然天天使用,就是因為太基礎所以很少有人系統認真的對這些數據類型進行分析,本文著重從整體的邏輯思路對這些基礎性的類型進行了介紹. ...
  • 下載:https://pan.baidu.com/s/1IakOOvmfltodm6w_taDcQg ...
  • 一.概述 Java不同於C/C++這類傳統的編譯型語言,也不同於php這一類動態的腳本語言。可以說Java是一種半編譯語言,我們所寫的類會先被編譯成.class文件,這個.class是一串二進位的位元組流。然後當要使用這個類的時候,就會將這個類對應的.class文件載入進記憶體中。而將這個.class的 ...
  • 一、contextMap中的數據操作 root根:List 元素1 元素2 元素3 元素4 元素5 contextMap:Map key value application Map key value name test session Map request Map attr Map 1、存數據: ...
  • 在上一篇《你真的懂ReentrantReadWriteLock嗎?》中我給大家留了一個引子,一個更高效同時可以避免寫饑餓的讀寫鎖 StampedLock。StampedLock實現了不僅多個讀不互相阻塞,同時在讀操作時不會阻塞寫操作。 為什麼StampedLock這麼神奇?能夠達到這種效果,它的核心 ...
  • Java集合框架中的List與Set 為列表,中在列表中的對象是由順序的排序的,並且是有重覆的對象。 簡單為:有序,有重覆。 為集合,在集合中的對象是不按照順序排列的,並且是沒有重覆的對象的。 簡單為:無序,無重覆。 為無序集合,無序無重覆; 為有序集合,有序有重覆; 案例 知識點 返回迭代的下一個 ...
  • %還是format 1、皇城PK Python中格式化字元串目前有兩種陣營:%和format,我們應該選擇哪種呢? 自從Python2.6引入了format這個格式化字元串的方法之後,我認為%還是format這根本就不算個問題。不信你往下看。 上面的代碼很明顯會拋出一個如下的TypeError: T ...
  • 閑來無事,買了一個雲伺服器來玩玩。想要做的第一件事情就是搭建web項目,查詢了網上的資料得知,部署web項目的步驟是 (1)配置java環境(這裡選擇的是jdk1.8) (2)配置tomcat伺服器(這裡選擇的是tomcat7) (3)上傳web項目 (4)訪問web項目 那麼就開始我們的第一步和第 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...