做者:賈俊江java
原文:https://mp.weixin.qq.com/s/gQfkOkR80MdtoTBP8hep6wnode
分佈式鎖和咱們日常講到的鎖原理基本同樣,目的就是確保在多個線程併發時,只有一個線程在同一刻操做這個業務或者說方法、變量。數據庫
在一個進程中,也就是一個 JVM 或者說應用中,咱們很容易去處理控制,在 jdk java.util 併發包中已經爲咱們提供了這些方法去加鎖,好比 Synchronized 關鍵字或者 Lock 鎖,均可以處理。安全
可是咱們如今的應用程序若是隻部署一臺服務器,那併發量是不好的,若是同時有上萬的請求,頗有可能形成服務器壓力過大而癱瘓。性能優化
想一想雙十一和大年三十晚上十點,瓜分支付寶紅包等業務場景,天然須要用到多臺服務器去同時處理這些業務,這些服務可能會有上百臺同時處理。服務器
可是咱們想想,若是有 100 臺服務器要處理分成包的業務,如今假設有 1 億的紅包,1 千萬我的分,金額隨機,那麼這個業務場景下,是否是必須確保這 1 千萬我的最後分的紅包金額總和等於 1 億?網絡
若是處理很差~~每人分到 100 萬,那馬雲爸爸估計大年初一,就得宣佈破產了~~架構
常規鎖會形成什麼狀況?併發
首先說一下咱們爲何要搞集羣。簡單理解就是,需求量(請求併發量)變大了,一個工人處理能力有限,那就多招一些工人來一塊兒處理。分佈式
假設 1 千萬個請求平均分配到 100 臺服務器上,每一個服務器接收 10w 的請求。
這 10w 個請求並非在同一秒中來的,多是在 1-2 個小時內,能夠聯想下咱們三十晚上開紅包,等到 10:20 開始,有的人立馬開了,有的人等到 12 點纔想起來。
那這樣的話,平均到每一秒上的請求也就不到 1 千個,這種壓力通常的服務器仍是能夠承受的:
等因而在每一個服務器中去分 1 億,也就是 10w 個用戶分了 1 億,最後總計有 100 個服務器,要分 100 億。
若是真這樣了,雖然說馬雲爸爸不會破產(據最新統計馬雲有 2300 億人民幣),那分成包的開發項目組,以及產品經理,能夠 GG了~
簡化結構圖以下:
分佈式鎖怎麼去處理?
那麼爲了解決這個問題,讓 1000 萬用戶只分 1 億,而不是 100 億,這個時候分佈式鎖就派上用處了。
分佈式鎖能夠把整個集羣就看成是一個應用同樣去處理,那麼也就須要這個鎖獨立於每個服務以外,而不是在服務裏面。
假設第一個服務器接收到用戶 1 的請求後,不能只在本身的應用中去判斷還有多少錢能夠分了,而須要去外部請求專門負責管理這 1 億紅包的人(服務),問他:哎,我這裏要分 100 塊,給我 100。
管理紅包的妹子(服務)一看,還有 1 個億,那好,給你 100 塊,而後剩下 99999900 塊。
第二個請求到來後,被服務器 2 獲取,繼續去詢問,管理紅包的妹子,我這邊要分 10 塊,管理紅包的妹子先查了下還有 99999900,那就說:好,給你 10 塊,那就剩下 99999890 塊。
等到第 1000w 個請求到來後,服務器 100 拿到請求,繼續去詢問,管理紅包的妹子,我要 100,妹子翻了翻白眼,對你說,就剩 1 塊了,愛要不要,那這個時候就只能給你 1 塊了(1 塊也是錢啊,買根辣條仍是能夠的)。
這些請求編號 1,2 不表明執行的前後順序,正式的場景下,應該是 100 臺服務器每一個服務器持有一個請求去訪問負責管理紅包的妹子(服務)。
那在管紅包的妹子那裏同時會接收到 100 個請求,這個時候就須要在負責紅包的妹子那裏加個鎖就能夠了(拋繡球),大家 100 個服務器誰拿到鎖(搶到繡球),誰就進來和我談,我給你分,其餘人就等着去吧。
通過上面的分佈式鎖的處理後,馬雲爸爸終於放心了,決定給紅包團隊每人加一個雞腿。
簡化的結構圖以下:
順便在此給你們推薦一個Java架構方面的交流學習羣:468897908,裏面有:Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,進羣免費領取學習資源!
分佈式鎖的實現有哪些?
說到分佈式鎖的實現,仍是有不少的,有數據庫方式的,有 Redis 分佈式鎖,有 Zookeeper 分佈式鎖等等。
咱們若是採用 Redis 做爲分佈式鎖,那麼上圖中負責「紅包的妹子(服務)」,就能夠替換成 Redis,請自行腦補。
①爲何 Redis 能夠實現分佈式鎖?
首先 Redis 是單線程的,這裏的單線程指的是網絡請求模塊使用了一個線程(因此不需考慮併發安全性),即一個線程處理全部網絡請求,其餘模塊仍用了多個線程。
在實際的操做中過程大體是這樣子的:服務器 1 要去訪問發紅包的妹子,也就是 Redis,那麼它會在 Redis 中經過"setnx key value" 操做設置一個 Key 進去,Value 是啥不重要,重要的是要有一個 Key,也就是一個標記。
並且這個 Key 你愛叫啥叫啥,只要全部的服務器設置的 Key 相同就能夠。
假設咱們設置一個,以下圖:
那麼咱們能夠看到會返回一個 1,那就表明了成功。
若是再來一個請求去設置一樣的 Key,以下圖:
這個時候會返回 0,那就表明失敗了。
那麼咱們就能夠經過這個操做去判斷是否是當前能夠拿到鎖,或者說能夠去訪問「負責發紅包的妹子」,若是返回 1,那我就開始去執行後面的邏輯,若是返回 0,那就說明已經被人佔用了,我就要繼續等待。
當服務器 1 拿到鎖以後,進行了業務處理,完成後,還須要釋放鎖,以下圖所示:
刪除成功返回 1,那麼其餘的服務器就能夠繼續重複上面的步驟去設置這個 Key,以達到獲取鎖的目的。
固然以上的操做是在 Redis 客戶端直接進行的,經過程序調用的話,確定就不能這麼寫,好比 Java 就須要經過 Jedis 去調用,可是整個處理邏輯基本都是同樣的。
經過上面的方式,咱們好像是解決了分佈式鎖的問題,可是想一想還有沒有什麼問題呢?
對,問題仍是有的,可能會有死鎖的問題發生,好比服務器 1 設置完以後,獲取了鎖以後,突然發生了宕機。
那後續的刪除 Key 操做就無法執行,這個 Key 會一直在 Redis 中存在,其餘服務器每次去檢查,都會返回 0,他們都會認爲有人在使用鎖,我須要等。
爲了解決這個死鎖的問題,咱們就須要給 Key 設置有效期了。設置的方式有 2 種:
第一種就是在 Set 完 Key 以後,直接設置 Key 的有效期 "expire key timeout" ,爲 Key 設置一個超時時間,單位爲 Second,超過這個時間鎖會自動釋放,避免死鎖。
這種方式至關於,把鎖持有的有效期,交給了 Redis 去控制。若是時間到了,你尚未給我刪除 Key,那 Redis 就直接給你刪了,其餘服務器就能夠繼續去 Setnx 獲取鎖。
第二種方式,就是把刪除 Key 權利交給其餘的服務器,那這個時候就須要用到 Value 值了,好比服務器 1,設置了 Value 也就是 Timeout 爲當前時間 +1 秒 。
這個時候服務器 2 經過 Get 發現時間已經超過系統當前時間了,那就說明服務器 1 沒有釋放鎖,服務器 1 可能出問題了,服務器 2 就開始執行刪除 Key 操做,而且繼續執行 Setnx 操做。
可是這塊有一個問題,也就是不光你服務器 2 可能會發現服務器 1 超時了,服務器 3 也可能會發現,若是恰好服務器 2 Setnx 操做完成,服務器 3 就接着刪除,是否是服務器 3 也能夠 Setnx 成功了?
那就等因而服務器 2 和服務器 3 都拿到鎖了,那就問題大了。這個時候怎麼辦呢?
這個時候須要用到「GETSET key value」命令了。這個命令的意思就是獲取當前 Key 的值,而且設置新的值。
假設服務器 2 發現 Key 過時了,開始調用 getset 命令,而後用獲取的時間判斷是否過時,若是獲取的時間仍然是過時的,那就說明拿到鎖了。
若是沒有,則說明在服務 2 執行 getset 以前,服務器 3 可能也發現鎖過時了,而且在服務器 2 以前執行了 getset 操做,從新設置了過時時間。
那麼服務器 2 就須要放棄後續的操做,繼續等待服務器 3 釋放鎖或者去監測 Key 的有效期是否過時。
這塊其實有一個小問題是,服務器 3 已經修改了有效期,拿到鎖以後,服務器 2 也修改了有效期,可是沒能拿到鎖。
可是這個有效期的時間已經被在服務器 3 的基礎上又增長一些,可是這種影響其實仍是很小的,幾乎能夠忽略不計。
②爲何 Zookeeper 可實現分佈式鎖?
百度百科是這麼介紹的:ZooKeeper 是一個分佈式的,開放源碼的分佈式應用程序協調服務,是 Google 的 Chubby 一個開源的實現,是 Hadoop 和 Hbase 的重要組件。
那對於咱們初次認識的人,能夠理解成 ZooKeeper 就像是咱們的電腦文件系統,咱們能夠在 d 盤中建立文件夾 a,而且能夠繼續在文件夾 a 中建立文件夾 a1,a2。
那咱們的文件系統有什麼特色?那就是同一個目錄下文件名稱不能重複,一樣 ZooKeeper 也是這樣的。
在 ZooKeeper 全部的節點,也就是文件夾稱做 Znode,並且這個 Znode 節點是能夠存儲數據的。
咱們能夠經過「 create /zkjjj nice」來建立一個節點,這個命令就表示,在根目錄下建立一個 zkjjj 的節點,值是 nice。
一樣這裏的值,和我在前面說的 Redis 中的同樣,沒什麼意義,你隨便給。
另外 ZooKeeper 能夠建立 4 種類型的節點,分別是:
首先說下持久性節點和臨時性節點的區別:
Zookeeper 有一個監聽機制,客戶端註冊監聽它關心的目錄節點,當目錄節點發生變化(數據改變、被刪除、子目錄節點增長刪除)等,Zookeeper 會通知客戶端。
在 Zookeeper 中如何加鎖?
下面咱們繼續結合咱們上面的分成包場景,描述下在 Zookeeper 中如何加鎖。
假設服務器 1,建立了一個節點 /zkjjj,成功了,那服務器 1 就獲取了鎖,服務器 2 再去建立相同的鎖,就會失敗,這個時候就只能監聽這個節點的變化。
等到服務器 1 處理完業務,刪除了節點後,他就會獲得通知,而後去建立一樣的節點,獲取鎖處理業務,再刪除節點,後續的 100 臺服務器與之相似。
注意這裏的 100 臺服務器並非挨個去執行上面的建立節點的操做,而是併發的,當服務器 1 建立成功,那麼剩下的 99 個就都會註冊監聽這個節點,等通知,以此類推。
可是你們有沒有注意到,這裏仍是有問題的,仍是會有死鎖的狀況存在,對不對?
當服務器 1 建立了節點後掛了,沒能刪除,那其餘 99 臺服務器就會一直等通知,那就完蛋了。
這個時候就須要用到臨時性節點了,咱們前面說過了,臨時性節點的特色是客戶端一旦斷開,就會丟失。
也就是當服務器 1 建立了節點後,若是掛了,那這個節點會自動被刪除,這樣後續的其餘服務器,就能夠繼續去建立節點,獲取鎖了。
可是咱們可能還須要注意到一點,就是驚羣效應:舉一個很簡單的例子,當你往一羣鴿子中間扔一塊食物,雖然最終只有一個鴿子搶到食物,但全部鴿子都會被驚動來爭奪,沒有搶到…
就是當服務器 1 節點有變化,會通知其他的 99 個服務器,可是最終只有 1 個服務器會建立成功,這樣 98 仍是須要等待監聽,那麼爲了處理這種狀況,就須要用到臨時順序性節點。
大體意思就是,以前是全部 99 個服務器都監聽一個節點,如今就是每個服務器監聽本身前面的一個節點。
假設 100 個服務器同時發來請求,這個時候會在 /zkjjj 節點下建立 100 個臨時順序性節點 /zkjjj/000000001,/zkjjj/000000002,一直到 /zkjjj/000000100,這個編號就等因而已經給他們設置了獲取鎖的前後順序了。
當 001 節點處理完畢,刪除節點後,002 收到通知,去獲取鎖,開始執行,執行完畢,刪除節點,通知 003~以此類推。