分散式鎖和我們平常講到的鎖原理基本一樣,目的就是確保在多個線程並發時,只有一個線程在同一刻操作這個業務或者說方法、變數。

在一個進程中,也就是一個 JVM 或者說應用中,我們很容易去處理控制,在 jdk java.util 並發包中已經為我們提供了這些方法去加鎖,比如 Synchronized 關鍵字或者 Lock 鎖,都可以處理。

但是我們現在的應用程序如果只部署一台伺服器,那並發量是很差的,如果同時有上萬的請求,很有可能造成伺服器壓力過大而癱瘓。

想想雙十一和大年三十晚上十點,瓜分支付寶紅包等業務場景,自然需要用到多台伺服器去同時處理這些業務,這些服務可能會有上百台同時處理。

但是我們想一想,如果有 100 台伺服器要處理分紅包的業務,現在假設有 1 億的紅包,1 千萬個人分,金額隨機,那麼這個業務場景下,是不是必須確保這 1 千萬個人最後分的紅包金額總和等於 1 億?

如果處理不好~~每人分到 100 萬,那馬雲爸爸估計大年初一,就得宣布破產了~~

常規鎖會造成什麼情況?

首先說一下我們為什麼要搞集群。簡單理解就是,需求量(請求並發量)變大了,一個工人處理能力有限,那就多招一些工人來一起處理。

假設 1 千萬個請求平均分配到 100 台伺服器上,每個伺服器接收 10w 的請求。

這 10w 個請求並不是在同一秒中來的,可能是在 1-2 個小時內,可以聯想下我們三十晚上開紅包,等到 10:20 開始,有的人立馬開了,有的人等到 12 點才想起來。

那這樣的話,平均到每一秒上的請求也就不到 1 千個,這種壓力一般的伺服器還是可以承受的:

  • 第一個用戶來分,請求到來後,需要在 1 億裡面給他分一部分錢,金額隨機,假設第一個人分到了 100,那就要在這 1 億中減去 100 塊,剩下 99999900 塊。
  • 第二個用戶再來分,金額隨機,這次分 200 塊,那就需要在剩下的 99999900 塊中再減去 200 塊,剩下 99999700 塊。
  • 等到第 10w 個用戶來,一看還有 1000w,那這 1000w 全成他的了。

等於是在每個伺服器中去分 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 個伺服器誰拿到鎖(搶到繡球),誰就進來和我談,我給你分,其他人就等著去吧。

經過上面的分散式鎖的處理後,馬雲爸爸終於放心了,決定給紅包團隊每人加一個雞腿。

簡化的結構圖如下:

分散式鎖的實現有哪些?

說到分散式鎖的實現,還是有很多的,有資料庫方式的,有 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 服務端就不再保存這個節點。
  • 順便也說下順序性節點,順序性節點是指,在創建節點的時候,ZooKeeper 會自動給節點編號比如 0000001,0000002 這種的。

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~以此類推。

關注微信公眾號「托尼的技術成長之路」


推薦閱讀:
相关文章