本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等git
首先,我先帶你認識一下什麼是鎖。github
在單機多線程環境中,咱們常常遇到多個線程訪問同一個共享資源(這裏須要注意的是:在不少地方,這種資源會稱爲臨界資源,但在今天這篇文章中,咱們統一稱之爲共享資源)的狀況。爲了維護數據的一致性,咱們須要某種機制來保證只有知足某個條件的線程才能訪問資源,不知足條件的線程只能等待,在下一輪競爭中從新知足條件時才能訪問資源。面試
這個機制指的是,爲了實現分佈式互斥,在某個地方作個標記,這個標記每一個線程都能看到,到標記不存在時能夠設置該標記,當標記被設置後,其餘線程只能等待擁有該標記的線程執行完成,並釋放該標記後,才能去設置該標記和訪問共享資源。這裏的標記,就是咱們常說的鎖。數據庫
也就是說,鎖是實現多線程同時訪問同一共享資源,保證同一時刻只有一個線程可訪問共享資源所作的一種標記。緩存
與普通鎖不一樣的是,分佈式鎖是指分佈式環境下,系統部署在多個機器中,實現多進程分佈式互斥的一種鎖。爲了保證多個進程能看到鎖,鎖被存在公共存儲(好比 Redis、Memcache、數據庫等三方存儲中),以實現多個進程併發訪問同一個臨界資源,同一時刻只有一個進程可訪問共享資源,確保數據的一致性。服務器
那什麼場景下須要使用分佈式鎖呢?多線程
好比,如今某電商要售賣某大牌吹風機(如下簡稱「吹風機」),庫存只有 2 個,但有 5個來自不一樣地區的用戶{A,B,C,D,E}幾乎同時下單,那麼這 2 個吹風機到底會花落誰家呢?併發
你可能會想,這還不簡單,誰先提交訂單請求,誰就購買成功唄。但實際業務中,爲了高併發地接受大量用戶訂單請求,不多有電商網站真正實施這麼簡單的措施。框架
此外,對於訂單的優先級,不一樣電商每每採起不一樣的策略,好比有些電商根據下單時間判斷誰能夠購買成功,而有些電商則是根據付款時間來判斷。但,不管採用什麼樣的規則去判斷誰能購買成功,都必需要保證吹風機售出時,數據庫中更新的庫存是正確的。爲了便於理解,我在下面的講述中,如下單時間做爲購買成功的判斷依據。分佈式
咱們能想到的最簡單方案就是,給吹風機的庫存數加一個鎖。當有一個用戶提交訂單後,後臺服務器給庫存數加一個鎖,根據該用戶的訂單修改庫存。而其餘用戶必須等到鎖釋放之後,才能從新獲取庫存數,繼續購買。
在這裏,吹風機的庫存就是共享資源,不一樣的購買者對應着多個進程,後臺服務器對共享資源加的鎖就是告訴其餘進程「關鍵重地,非請勿入」。
但問題就這樣解決了嗎?固然沒這麼簡單。
想象一下,用戶 A 想買 1 個吹風機,用戶 B 想買 2 個吹風機。在理想狀態下,用戶 A 網速好先買走了 1 個,庫存還剩下 1 個,此時應該提示用戶 B 庫存不足,用戶 B 購買失敗。但實際狀況是,用戶 A 和用戶 B 同時獲取到商品庫存還剩 2 個,用戶 A 買走 1 個,在用戶 A 更新庫存以前,用戶 B 又買走了 2 個,此時用戶 B 更新庫存,商品還剩 0 個。這時,電商就頭大了,總共 2 個吹風機,卻賣出去了 3 個。
不難看出,若是隻使用單機鎖將會出現不可預知的後果。所以,在高併發場景下,爲了保證臨界資源同一時間只能被一個進程使用,從而確保數據的一致性,咱們就須要引入分佈式鎖了。
此外,在大規模分佈式系統中,單個機器的線程鎖沒法管控多個機器對同一資源的訪問,這時使用分佈式鎖,就能夠把整個集羣看成一個應用同樣去處理,實用性和擴展性更好。
接下來,我帶你看看實現分佈式鎖的 3 種主流方法,即:
要實現分佈式鎖,最簡單的方式就是建立一張鎖表,而後經過操做該表中的數據來實現。
當咱們要鎖住某個資源時,就在該表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。數據庫對共享資源作了惟一性約束,若是有多個請求被同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,操做成功的那個線程就得到了訪問共享資源的鎖,能夠進行操做。
基於數據庫實現的分佈式鎖,是最容易理解的。可是,由於數據庫須要落到硬盤上,頻繁讀取數據庫會致使 IO 開銷大,所以這種分佈式鎖適用於併發量低,對性能要求低的場景。對於雙 十一、雙 12 等需求量激增的場景,數據庫鎖是沒法知足其性能要求的。而在平日的購物中,咱們能夠在局部場景中使用數據庫鎖實現對資源的互斥訪問。
下面,咱們仍是以電商售賣吹風機的場景爲例。吹風機庫存是 2 個,有 5 個來自不一樣地區的用戶{A,B,C,D,E}想要購買,其中用戶 A 想買 1 個,用戶 B 想買 2 個,用戶 C 想買 1個。
用戶 A 和用戶 B 幾乎同時下單,但用戶 A 的下單請求最早到達服務器。所以,該商家的產品數據庫中增長了一條關於用戶 A 的記錄,用戶 A 得到了鎖,他的訂單請求被處理,服務器修改吹風機庫存數,減去 1 後還剩下 1 個。
當用戶 A 的訂單請求處理完成後,有關用戶 A 的記錄被刪除,服務器開始處理用戶 B 的訂單請求。這時,庫存只有 1 個了,沒法知足用戶 B 的訂單需求,所以用戶 B 購買失敗。
從數據庫中,刪除用戶 B 的記錄,服務器開始處理用戶 C 的訂單請求,庫存中 1 個吹風機知足用戶 C 的訂單需求。因此,數據庫中增長了一條關於用戶 C 的記錄,用戶 C 得到了鎖,他的訂單請求被處理,服務器修改吹風機數量,減去 1 後還剩下 0 個。
能夠看出,基於數據庫實現分佈式鎖比較簡單,絕招在於建立一張鎖表,爲申請者在鎖表裏創建一條記錄,記錄創建成功則得到鎖,消除記錄則釋放鎖。該方法依賴於數據庫,主要有兩個缺點:
數據庫的性能限制了業務的併發量,那麼對於雙 十一、雙 12 等需求量激增的場景是否有解決方法呢?
基於緩存實現分佈式鎖的方式,很是適合解決這種場景下的問題。所謂基於緩存,也就是說把數據存放在計算機內存中,不須要寫入磁盤,減小了 IO 讀寫。接下來,我以 Redis 爲例與你展開這部份內容。
Redis 一般可使用 setnx(key, value) 函數來實現分佈式鎖。key 和 value 就是基於緩存的分佈式鎖的兩個屬性,其中 key 表示鎖 id,value = currentTime + timeOut,表示當前時間 + 超時時間。也就是說,某個進程得到 key 這把鎖後,若是在 value 的時間內未釋放鎖,系統就會主動釋放鎖。
setnx 函數的返回值有 0 和 1:
我仍是以電商售賣吹風機的場景爲例,和你說明基於緩存實現的分佈式鎖,假設如今庫存數量是足夠的。
用戶 A 的請求由於網速快,最早到達 Server2,setnx 操做返回 1,並獲取到購買吹風機的鎖;用戶 B 和用戶 C 的請求,幾乎同時到達了 Server1 和 Server3,但由於這時 Server2獲取到了吹風機數據的鎖,因此只能加入等待隊列。
Server2 獲取到鎖後,負責管理吹風機的服務器執行業務邏輯,只用了 1s 就完成了訂單。訂單請求完成後,刪除鎖的 key,從而釋放鎖。此時,排在第二順位的 Server1 得到了鎖,能夠訪問吹風機的數據資源。但不巧的是,Server1 在完成訂單後發生了故障,沒法主動釋放鎖。
因而,排在第三順位的 Server3 只能等設定的有效時間(好比 30 分鐘)到期,鎖自動釋放後,才能訪問吹風機的數據資源,也就是說用戶 C 只能到 00:30:01 之後才能繼續搶購。
總結來講,Redis 經過隊列來維持進程訪問共享資源的前後順序。Redis 鎖主要基於 setnx函數實現分佈式鎖,當進程經過 setnx<key,value> 函數返回 1 時,表示已經得到鎖。排在後面的進程只能等待前面的進程主動釋放鎖,或者等到時間超時才能得到鎖。
相對於基於數據庫實現分佈式鎖的方案來講,基於緩存實現的分佈式鎖的優點表如今如下幾個方面:
這個方案的不足是,經過超時時間來控制鎖的失效時間,並非十分靠譜,由於一個進程執行時間可能比較長,或受系統進程作內存回收等影響,致使時間超時,從而不正確地釋放了鎖。
爲了解決基於緩存實現的分佈式鎖的這些問題,咱們再來看看基於 ZooKeeper 實現的分佈式鎖吧。
ZooKeeper 基於樹形數據存儲結構實現分佈式鎖,來解決多個進程同時訪問同一臨界資源時,數據的一致性問題。ZooKeeper 的樹形數據存儲結構主要由 4 種節點構成:
根據它們的特徵,ZooKeeper 基於臨時順序節點實現了分佈鎖。
仍是以電商售賣吹風機的場景爲例。假設用戶 A、B、C 同時在 11 月 11 日的零點整提交了購買吹風機的請求,ZooKeeper 會採用以下方法來實現分佈式鎖:
例如,用戶 B 也想要買吹風機,但在他以前,用戶 C 想看看吹風機的庫存量。所以,用戶B 只能等用戶 A 買完吹風機、用戶 C 查詢完庫存量後,才能購買吹風機。
能夠看到,使用 ZooKeeper 能夠完美解決設計分佈式鎖時遇到的各類問題,好比單點故障、不可重入、死鎖等問題。雖然 ZooKeeper 實現的分佈式鎖,幾乎能涵蓋全部分佈式鎖的特性,且易於實現,但須要頻繁地添加和刪除節點,因此性能不如基於緩存實現的分佈式鎖。
我經過一張表格來對比一下這三種方式的特色,以方便你理解、記憶。
總結來講,ZooKeeper 分佈式鎖的可靠性最高,有封裝好的框架,很容易實現分佈式鎖的功能,而且幾乎解決了數據庫鎖和緩存式鎖的不足,所以是實現分佈式鎖的首選方法。
從上述分析可看出,爲了確保分佈式鎖的可用性,咱們在設計時應考慮到如下幾點:
互斥性,即在分佈式系統環境下,分佈式鎖應該能保證一個資源或一個方法在同一時間只
能被一個機器的一個線程或進程操做。
具有鎖失效機制,防止死鎖。即便有一個進程在持有鎖的期間由於崩潰而沒有主動解鎖,
也能保證後續其餘進程能夠得到鎖。
可重入性,即進程未釋放鎖時,能夠屢次訪問臨界資源。
有高可用的獲取鎖和釋放鎖的功能,且性能要好。
本文以電商購物爲例,首先帶你剖析了什麼是分佈式鎖,以及爲何須要分佈式鎖;而後,與你介紹了三種實現分佈式鎖的方法,包括基於數據庫實現、基於緩存實現(以 Redis 爲例),以及基於 ZooKeeper 實現。
分佈式鎖是解決多個進程同時訪問臨界資源的經常使用方法,在分佈式系統中很是常見,好比開源的 ZooKeeper、Redis 中就有所涉及。經過今天這篇文章對分佈式鎖原理及方法的講解,我相信你會發現分佈式鎖再也不那麼神祕、難懂,而後以此爲基礎對分佈式鎖進行更深刻的學習和應用。
接下來,我把今天的內容經過下面的一張思惟導圖再全面總結下。