Dubbo+Zookeeper(一)Zookeeper初識
- 2019 年 10 月 24 日
- 筆記
前面花了一段時間去學習SpringCloud的相關知識,主要是理解微服務的概念並使用SpringCloud的一系列組件實現微服務落地。學習這些組件本身是簡單的,跟着操作一遍基本就會了,這也得益於Springboot給我們帶來了很多便利。實際的應用中也許還會碰到一些坑,但只要我們掌握基本的原理就能夠解決。
前面也講了微服務的解決方案有兩個,一個是SpringCloud,另外一個就是Dubbo+Zookeeper,下面我們來學習Dubbo+Zookeeper實現微服務。
在學習微服務的第一篇中,理解了微服務的概念,比較了微服務與單體應用之前的優勢和劣勢,講了要實現微服務主要的技術點,最重要的兩塊在於服務之間的通信和服務治理。
Dubbo+Zookeeper要實現微服務,就必須解決這兩個技術點,Dubbo是一個RPC通信框架,它可以實現服務之間的通信。ZooKeeper 是一種分佈式協調服務,用於管理大型主機。在分佈式環境中協調和管理服務是一個複雜的過程。
一、
上面說Zookeeper是一個分佈式協調技術,那麼我們就得先來學習什麼是分佈式協調技術。分佈式協調技術主要用來解決分佈式環境當中多個進程之間的同步控制,讓他們有序的去訪問某種臨界資源,防止造成”臟數據”的後果。
首先,要明白我們為什麼需要分佈式鎖,一個簡單的例子,一般系統上都有一些定時任務,比如做一些數據的清算,如果我們部署了多台服務器,那在這個時候每台服務器都會執行這個定時任務,如果沒有一個鎖機制,那按照道理來說,這個定時任務就會被執行多次,這樣就非常有可能出現臟數據。
這裡指的是一個定時任務,既然是定時任務,那麼它必然會是同一個時刻進入,這個不需要高並發,我們也需要考慮。還有一種情況是高並發情況下,在分佈式環境中,很大概率會存在多個節點上的進程同時訪問某一個方法,而很多時候,我們需要保證一個方法在同一時間內只能被同一個線程執行,在單機環境中,Java中其實提供了很多並發處理相關的API,但是這些API在分佈式場景中就無能為力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。所以針對分佈式鎖的實現目前有多種方案。
我們需要的分佈式鎖應該是怎麼樣的?(這裡以方法鎖為例,資源鎖同理)
-
可以保證在分佈式部署的應用集群中,同一個方法在同一時間只能被一台機器上的一個線程執行。
-
這把鎖要是一把可重入鎖(避免死鎖)
-
這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
-
有高可用的獲取鎖和釋放鎖功能
-
獲取鎖和釋放鎖的性能要好
實現分佈式鎖有幾種方案:
-
Memcached:利用 Memcached 的
add
命令。此命令是原子性操作,只有在key
不存在的情況下,才能add
成功,也就意味着線程得到了鎖。 -
Redis:和 Memcached 的方式類似,利用 Redis 的
setnx
命令。此命令同樣是原子性操作,只有在key
不存在的情況下,才能set
成功。 -
Zookeeper:利用 Zookeeper 的順序臨時節點,來實現分佈式鎖和等待隊列。Zookeeper 設計的初衷,就是為了實現分佈式鎖服務的。
-
Chubby:Google 公司實現的粗粒度分佈式鎖服務,底層利用了 Paxos 一致性算法。
二、分佈式鎖實現的基本原理
要實現鎖,那最基本的功能就有三個:加鎖,解鎖,鎖超時,我們用redis的實現方式來簡單的介紹下分佈式鎖的基本原理,以下代碼都為偽代碼。
2.1 加鎖
redis實現加鎖最簡單的操作就是是使用 setnx
命令,其中的key可以根據業務名稱來命名,基本模式是命名空間+對應的參數,比如我們要鎖住某個商品庫存,那我們可以用庫存的id作為參數進行加鎖,其實這裡我們在不同的方法上可以加上相同的鎖,比如,有兩個方法都需要對庫存進行處理,雖然他們並不是一個方法,但我們用同一個命名空間和參數也可以鎖住。
setnx(lock_sale_商品ID,1)
當我們在進行set方法時,如果key已經存在,說明已經有其他線程得到了鎖,搶鎖失敗,就需要進行等待,如果key不存在,那說明得到了鎖,方法就正常運行。
2.2 解鎖
既然有加鎖過程,那也就有解鎖過程,線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行 del
指令。一旦刪除了key,那其他線程就可以正常的獲得鎖了。
del(lock_sale_商品ID)
2.3 鎖超時
鎖超時是什麼意思呢?如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住(死鎖),別的線程再也別想進來。所以,setnx
的 key
必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。setnx
不支持超時參數,所以需要額外的指令。
expire(lock_sale_商品ID, 30)
那整體的偽代碼就是:
if(setnx(lock_sale_商品ID,1) == 1){ expire(lock_sale_商品ID,30) try { do something ...... } finally { del(lock_sale_商品ID) } }
2.4 存在的問題
redis實現分佈式鎖基本就是按照這個思路,但是這裡存在一些問題。
setnx
和 expire
的非原子性
如果我們使用java的指令來進行超時控制,加鎖和設置超時時間不是一個原子操作,那如果加鎖後進程掛掉了,那此時不會執行設置超時代碼,那這個鎖就永遠不會被解,這樣顯然是不行的。
解決方案:使用set指令,直接帶入超時時間,那這個超時問題就交給redis本身去解決,這樣即使java進程出現了問題,redis本身也能夠進行解鎖。
set(lock_sale_商品ID,1,30,NX)
誤刪
某一個線程A成功得到了鎖,並設置了超時時間30秒,但由於這個方法本身執行比較慢,超過了30秒還沒執行完,鎖過期自動釋放了,然後線程B得到鎖,隨後線程A執行完了,它執行了解鎖任務,刪除了鎖,但我們知道這個時候解的鎖並不是自己的,而是線程B的鎖,這就是誤刪了線程B的鎖。
解決方案:可以在加鎖的時候把當前的線程 ID 當做 value
,並在刪除之前驗證 key
對應的 value
是不是自己線程的 ID。
那加鎖的代碼:
String threadId = Thread.currentThread().getId() set(key,threadId ,30,NX)
解鎖的代碼:
if(threadId .equals(redisClient.get(key))){ del(key) }
出現並發的可能性
在上面的描述中,如果一個方法執行時間過長,那還是有可能會出現並發的可能性,但如果過期時間設置太長也會出現他獲取鎖的線程就可能要平白的多等一段時間。
了解完redis實現分佈式鎖後,我們對分佈式鎖有了一個感性的認識,現在來學習Zookeeper
三、Zookeeper初識
Apache ZooKeeper是由集群(節點組)使用的一種服務,用於在自身之間協調,並通過穩健的同步技術維護共享數據。ZooKeeper本身是一個分佈式應用程序,為寫入分佈式應用程序提供服務。
ZooKeeper提供的常見服務如下 :
-
命名服務 – 按名稱標識集群中的節點。它類似於DNS,但僅對於節點。
-
配置管理 – 加入節點的最近的和最新的系統配置信息。
-
集群管理 – 實時地在集群和節點狀態中加入/離開節點。
-
選舉算法 – 選舉一個節點作為協調目的的leader。
-
鎖定和同步服務 – 在修改數據的同時鎖定數據。此機制可幫助你在連接其他分佈式應用程序(如Apache HBase)時進行自動故障恢復。
-
高度可靠的數據註冊表 – 即使在一個或幾個節點關閉時也可以獲得數據。
分佈式應用程序提供了很多好處,但它們也拋出了一些複雜和難以解決的挑戰。ZooKeeper框架提供了一個完整的機制來克服所有的挑戰。競爭條件和死鎖使用故障安全同步方法進行處理。另一個主要缺點是數據的不一致性,ZooKeeper使用原子性解析。
以下是使用ZooKeeper的好處:
-
簡單的分佈式協調過程
-
同步 – 服務器進程之間的相互排斥和協作。此過程有助於Apache HBase進行配置管理。
-
有序的消息
-
序列化 – 根據特定規則對數據進行編碼。確保應用程序運行一致。這種方法可以在MapReduce中用來協調隊列以執行運行的線程。
-
可靠性
-
原子性 – 數據轉移完全成功或完全失敗,但沒有事務是部分的。
3.1 Zookeeper 整體架構
我們先了解Zookeeper的整體架構,先看下面的圖表:
-
-
Server(服務器):服務器,我們的ZooKeeper總體中的一個節點,為客戶端提供所有的服務。向客戶端發送確認碼以告知服務器是活躍的。
-
Ensemble: ZooKeeper服務器組。形成ensemble所需的最小節點數為3。
-
Leader:服務器節點,如果任何連接的節點失敗,則執行自動恢復。Leader在服務啟動時被選舉。
-
3.2 Zookeeper 數據模型
Zookeeper 的數據模型是什麼樣子呢?它很像數據結構當中的樹,也很像文件系統的目錄。
ZooKeeper節點稱為 znode 。每個znode由一個名稱標識,並用路徑(/)序列分隔,這樣的層級結構,讓每一個 Znode 節點擁有唯一的路徑,就像命名空間一樣對不同信息作出清晰的隔離。
那znode里有包含哪些元素呢?
-
-
ACL:記錄 Znode 的訪問權限,即哪些人或哪些 IP 可以訪問本節點。
-
stat:包含 Znode 的各種元數據,比如事務 ID、版本號、時間戳、大小等等。存儲在znode中的數據總量是數據長度。你最多可以存儲1MB的數據。
-
Znode的類型
Znode被分為持久(persistent)節點,順序(sequential)節點和臨時(ephemeral)節點。
-
持久節點 – 即使在創建該特定znode的客戶端斷開連接後,持久節點仍然存在。默認情況下,除非另有說明,否則所有znode都是持久的。
-
臨時節點 – 客戶端活躍時,臨時節點就是有效的。當客戶端與ZooKeeper集合斷開連接時,臨時節點會自動刪除。因此,只有臨時節點不允許有子節點。如果臨時節點被刪除,則下一個合適的節點將填充其位置。臨時節點在leader選舉中起着重要作用。
-
順序節點 – 順序節點可以是持久的或臨時的。當一個新的znode被創建為一個順序節點時,ZooKeeper通過將10位的序列號附加到原始名稱來設置znode的路徑。例如,如果將具有路徑 /myapp 的znode創建為順序節點,則ZooKeeper會將路徑更改為 /myapp0000000001 ,並將下一個序列號設置為0000000002。如果兩個順序節點是同時創建的,那麼ZooKeeper不會對每個znode使用相同的數字。順序節點在鎖定和同步中起重要作用。
Znode支持的操作及暴露的API:
create /path data
創建一個名為/path的znode,數據為data。
delete /path
刪除名為/path的znode。
exists /path
檢查是否存在名為/path的znode
setData /path data
設置名為/path的znode的數據為data
getData /path
返回名為/path的znode的數據
getChildren /path
返回所有/path節點的所有子節點列表
3.3 Watches
監視是一種簡單的機制,使客戶端收到關於ZooKeeper集合中的更改的通知。客戶端可以在讀取特定znode時設置Watches。Watches會向註冊的客戶端發送任何znode(客戶端註冊表)更改的通知。
Znode更改是與znode相關的數據的修改或znode的子項中的更改。只觸發一次watches。如果客戶端想要再次通知,則必須通過另一個讀取操作來完成。當連接會話過期時,客戶端將與服務器斷開連接,相關的watches也將被刪除。
具體的交互如下:
客戶端調用 getData
方法,watch
參數是 true
。服務端接到請求,返回節點數據,並且在對應的哈希表裡插入被 Watch 的 Znode 路徑,以及 Watcher 列表。
當被 Watch 的 Znode 已刪除,服務端會查找哈希表,找到該 Znode 對應的所有 Watcher,異步通知客戶端,並且刪除哈希表中對應的 Key-Value。
Zookeeper的基本概念是這些,下一篇將深入其基本原理,學習Zookeeper一致性實現和Zookeeper是如何實現分佈式鎖的。