[TOC]
zookeeper動(dòng)物管理員全局把控。提供了配置管理、服務(wù)發(fā)現(xiàn)等服務(wù)。其本身也是可以集群化的。實(shí)現(xiàn)上是基于觀察者模式。不想eureka/consul等同類產(chǎn)品需要心跳機(jī)制。他本身支持觀察與主動(dòng)觸發(fā)機(jī)制;千里之行始于足下,我們已經(jīng)探索了eureka、consul兩個(gè)服務(wù)注冊(cè)的中間件了。今天我們繼續(xù)學(xué)習(xí)另外一個(gè)作為服務(wù)注冊(cè)的服務(wù)。
本文將從zookeeper單機(jī)到集群的安裝講解;在從集群leader選舉機(jī)制的講解及數(shù)據(jù)同步的梳理。到最終的基于zookeeper實(shí)現(xiàn)的配置管理及分布式鎖的應(yīng)用。從點(diǎn)到面在到應(yīng)用帶你體會(huì)一把過山車
簡(jiǎn)介
- Zookeeper 大家都知道是動(dòng)物管理員的意思。在大數(shù)據(jù)全家桶中他的作用也是管理。下面我們分別從安裝到使用來看看zk的優(yōu)美
中心化
| 服務(wù) | 特點(diǎn) | 中心化 | CAP |
|---|---|---|---|
| eureka | peer to peer 每個(gè)eureka服務(wù)默認(rèn)都會(huì)向集群中其他server注冊(cè)及拉去信息 | 去中心化 | AP |
| consul | 通過其中節(jié)點(diǎn)病毒式蔓延至整個(gè)集群 | 多中心化 | CP |
| zookeeper | 一個(gè)leader多個(gè)followes | 中心化 | CP |
事務(wù)
上面提到zookeeper是一個(gè)leader多個(gè)followers。
除了中心化思想外,zookeeper還有個(gè)重要的特性就是事務(wù)。zookeeper數(shù)據(jù)操作是具有原子性的。
如何理解zookeeper的事務(wù)呢,其實(shí)內(nèi)部是通過版本管理數(shù)據(jù)實(shí)現(xiàn)事務(wù)性的。zookeeper每個(gè)客戶端初始化時(shí)都會(huì)初始化一個(gè)operation。每個(gè)client連接是都有個(gè)內(nèi)部的session管理。同一個(gè)session操作都會(huì)有對(duì)應(yīng)的版本記錄,zxid這樣能保證數(shù)據(jù)的一個(gè)一致性。
downlaod
單機(jī)安裝
- 在上面的地址下載后進(jìn)行解壓
tar -zxvf apache-zookeeper-3.5.9-bin.tar.gz

- 官方提供的相當(dāng)于是個(gè)模板,這個(gè)時(shí)候直接啟動(dòng)會(huì)報(bào)錯(cuò)的。

- 根據(jù)報(bào)錯(cuò)信息我們知道缺失zoo.cfg默認(rèn)配置文件。在conf目錄下官方給我們提供了zoo_sample.cfg模板配置文件。我們只需要復(fù)制改文件為zoo.cfg在此基礎(chǔ)上進(jìn)行修該
cp zoo_sample.cfg zoo.cfg

- 我們修改下data路徑就可以啟動(dòng)了。

- 啟動(dòng)之后通過'zkServer.sh status'查看zookeeper運(yùn)行狀態(tài)。我們可以看到此時(shí)是單機(jī)模式啟動(dòng)的。

- 通過jps我們也能夠看到zookeeper啟動(dòng)成功了。
集群搭建
這里的集群為了方便就演示偽集群版。即在一臺(tái)服務(wù)器上布置三臺(tái)zk服務(wù)。
mkdir {zk1,zk2,zk3}, 首先創(chuàng)建zk1,zk2,zk3三個(gè)文件夾存放zk將之前解壓好的zk文件夾分別復(fù)制到三個(gè)創(chuàng)建zk中

- 在zookeeper進(jìn)群中,每臺(tái)服務(wù)都需要有一個(gè)編號(hào)。我們還需要寫入每臺(tái)機(jī)器的編號(hào)

- 然后重復(fù)我們單機(jī)版的過程。在根目錄創(chuàng)建data文件夾,然后修改conf中對(duì)應(yīng)的data配置。只不過這里還需要我們修改一下端口號(hào)。因?yàn)樵谕慌_(tái)機(jī)器上所以需要不同的端口號(hào)才行。
- 最后新增集群內(nèi)部通訊端口
server.1=192.168.44.130:28881:38881
server.2=192.168.44.130:28882:38882
server.3=192.168.44.130:28883:38883
- 上述的端口只要保證可用就行了。

上面是zk1的配置,其他讀者自行配置。
在配置總我們多了server的配置。這個(gè)server是集群配置的重點(diǎn)
server.1=192.168.44.139:28881:38881
- server是固定寫法
- .1 : 1就是我們之前每臺(tái)zk服務(wù)寫入的myid里的數(shù)字
- 192.168.44.130: 表示我們zk所在服務(wù)ip
- 28881: 在zookeeper集群中l(wèi)eader和follewers數(shù)據(jù)備份通信端口
- 38881: 選舉機(jī)制的端口

- 上面我是通過zkServer啟動(dòng)不同配置文件。你們也可以在不同的zk包下分別啟動(dòng)。最終效果是一樣的。啟動(dòng)完成之后我們通過jps查看可以看到多了三個(gè)zk服務(wù)。在單機(jī)版的時(shí)候jps我們知道zk的主啟動(dòng)是QuorumPeerMain。

上圖是其中一個(gè)zk服務(wù)的目錄結(jié)構(gòu)。我們可以看到data目錄有數(shù)據(jù)產(chǎn)生了。這是zk集群?jiǎn)?dòng)之后生成的文件。
集群?jiǎn)?dòng)完成了。但是我們現(xiàn)在對(duì)zk集群好像還是沒有太大的感知。比如說我們不知道誰是leader。 可以通過如下命令查看每臺(tái)zk的角色

- 我們的zk2是leader角色。其他是follower
Cli連接
- 在zookeeper包中還有一個(gè)zkCli.sh這個(gè)是zk的客戶端。通過他我們可以連接zookeeper服務(wù)并進(jìn)行操作。
zkCli.sh -server 192.168.44.131:1181 可以連接zk服務(wù)
- 下面我們測(cè)試下對(duì)zk的操作會(huì)不會(huì)是集群化的。

我們zkCli連接了zk1服務(wù)1181,并且創(chuàng)建的一個(gè)zk節(jié)點(diǎn)名為node。我們?cè)谌k2服務(wù)上同樣可以看到這個(gè)node節(jié)點(diǎn)。
至此,我們zookeeper集群搭建完成,并且測(cè)試也已經(jīng)通過了。
集群容錯(cuò)
Master選舉

- 我們已上述集群?jiǎn)?dòng)是為例,簡(jiǎn)述下集群選舉流程。
①、zk1啟動(dòng)時(shí),這個(gè)時(shí)候集群中只有一臺(tái)服務(wù)就是zk1。此時(shí)zk1給集群投票自然被zk1自己獲取。 此時(shí)zk1有一票
②、zk2啟動(dòng)時(shí),zk1,zk2都會(huì)都一票給集群。因?yàn)檫M(jìn)群中zk1(myid)小于zk2(myid),所以這兩票被zk2獲取。這里為什么會(huì)是zk2獲取到呢。zookeeper節(jié)點(diǎn)都一份坐標(biāo)zk=(myid,zxid);myid是每個(gè)zk服務(wù)配置的唯一項(xiàng)。zxid是zk服務(wù)的一個(gè)64位內(nèi)容。高32沒master選舉一次遞增一次并同時(shí)清空低32位。低32位是每發(fā)生一次數(shù)據(jù)事務(wù)遞增一次。所以zxid最高說明此zk服務(wù)數(shù)據(jù)越新。
③、zk2獲得兩票后,此時(shí)已經(jīng)獲得了集群半數(shù)以上的票數(shù),少數(shù)服從多數(shù)此時(shí)zk2已經(jīng)是準(zhǔn)leader了同時(shí)zk1切換為following 。此時(shí)zk1已經(jīng)是zk2的跟班了
④、zk3啟動(dòng)時(shí),按道理zk3應(yīng)該會(huì)收到三票。但是因?yàn)閦k1已經(jīng)站隊(duì)到zk2了。zk2作為準(zhǔn)leader是不可能給zk3投票的。所以zk3最多只有自己一票,zk3明知zk2獲得半數(shù)以上,已經(jīng)是民意所歸了。所以zk3為了自己的前途也就將自己的一票投給了zk2.
- zookeeper的投票選舉機(jī)制赤裸裸的就是一個(gè)官場(chǎng)。充滿的人心
leader宕機(jī)重新選舉
其實(shí)在啟動(dòng)階段zk2獲取到兩張投票是有一個(gè)PK的邏輯在里面的。上述啟動(dòng)階段的投票是個(gè)人的一個(gè)抽象化理解。
在上面說myid高的不會(huì)給myid低的投票實(shí)際上是一種片面的理解。實(shí)際上是會(huì)進(jìn)行投票的,投票之后會(huì)進(jìn)行兩張票PK,將權(quán)重高的一張票投出去選舉leader。有集群管理者進(jìn)行統(tǒng)計(jì)投票并計(jì)數(shù)。
下面我們來看看重新選舉是的邏輯。也是真正的leader選舉的邏輯。
①、zk2服務(wù)掛了,這個(gè)時(shí)候zk1,zk3立馬切換為looking狀態(tài),并分別對(duì)集群內(nèi)其他服務(wù)進(jìn)行投票
②、zk1收到自己的和其他服務(wù)投過來的票(1,0)、(3,0) 。zk1會(huì)斟酌這兩張票,基于我們提到的算法num=10*zxid+myid ,所以zk1會(huì)將(3,0)這張票投入計(jì)數(shù)箱中
③、zk3收到兩種票(3,0)、(1,0),同樣會(huì)將(3,0)投入計(jì)數(shù)箱
④、最終統(tǒng)計(jì)zk3獲得兩票勝出。
- 上面的選舉才是真正的選舉。啟動(dòng)時(shí)期我們只是加入了我們自己的理解在里面。選舉完之后服務(wù)會(huì)切換成leader、follower狀態(tài)進(jìn)行工作
數(shù)據(jù)同步

- 同樣先上圖
- client如果直接將數(shù)據(jù)變更請(qǐng)求發(fā)送到leader端,則直接從圖中第三步開始發(fā)送proposal請(qǐng)求等待過半機(jī)制后再發(fā)送commit proposal。follower則會(huì)開始更新本地zxid并同步數(shù)據(jù)。
- 如果client發(fā)送的是follower,則需要follower先將請(qǐng)求轉(zhuǎn)發(fā)至leader然后在重復(fù)上面的步驟。
- 在數(shù)據(jù)同步期間為了保障數(shù)據(jù)強(qiáng)一致性。leader發(fā)送的proposal都是有序的。follower執(zhí)行的數(shù)據(jù)變更也都是有順序的。這樣能保證數(shù)據(jù)最終一致性。
在一段時(shí)間內(nèi)比如說需要對(duì)變量a=5和1=1操作。如果a=5和a=1是兩個(gè)事物。如果leader通知follower進(jìn)行同步,zk1先a=1在a=5。則zk1中的a為5.zk3反之來則zk3中a=1;這樣就會(huì)造成數(shù)不一致。但是zookeeper通過znode節(jié)點(diǎn)有序排列保證了follower數(shù)據(jù)消費(fèi)也是有序的。在莫一時(shí)刻zk1執(zhí)行了a=5,這時(shí)候client查了zk1的a是5,雖然表面上是臟數(shù)據(jù)實(shí)際上是zk1未執(zhí)行完。等待zk1執(zhí)行完a=1.這就叫數(shù)據(jù)<red>最終一致性</red>。 - 下面一張圖可能更加的形象,來自于網(wǎng)絡(luò)圖片

特色功能
服務(wù)治理
- 在我們eureka、consul章節(jié)已經(jīng)介紹了springcloud注冊(cè)的細(xì)節(jié)了。今天我們還是同樣的操作。已payment和order模塊來講服務(wù)注冊(cè)到zookeeper上??纯葱Ч?/li>
<!-- SpringBoot整合zookeeper客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
- 配置文件配置添加zookeeper
spring:
cloud:
zookeeper:
connect-string: 192.168.44.131:2181
- 和consul一樣,這里的注冊(cè)會(huì)將spring.application.name值注冊(cè)過去。eureka是將spring.application.name的大寫名稱注冊(cè)過去。這個(gè)影響的就是order訂單中調(diào)用的地址區(qū)別。
- 還是一樣的操作。啟動(dòng)order、兩個(gè)payment之后我們調(diào)用
http://localhost/order/getpayment/123可以看到結(jié)果是負(fù)載均衡了。
配置管理
- 熟悉springcloud的都知道,springcloud是有一個(gè)配置中心的。里面主要借助git實(shí)現(xiàn)配置的實(shí)時(shí)更新。具體細(xì)節(jié)我們后面章節(jié)會(huì)慢慢展開,本次我們展示通過zookeeper實(shí)現(xiàn)配置中心管理。
- 首先我們?cè)谖覀兊膒ayment模塊繼續(xù)開發(fā),引入zookeeper-config模塊
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-config</artifactId>
</dependency>
- 然后我們需要有個(gè)儲(chǔ)備知識(shí),在spring加載配置文件的順序,會(huì)先加載bootstrap文件然后是application文件。這里bootstrap我們也用yml格式文件。
spring:
application:
name: cloud-payment-service
profiles:
active: dev
cloud:
zookeeper:
connect-string: 192.168.44.131:2181
config:
enabled: true
root: config
profileSeparator: ','
discovery:
enabled: true
enabled: true
- 上面這部分需要解釋下
| key | 解釋 |
|---|---|
| spring.application.name | 服務(wù)名 |
| spring.profiles | 環(huán)境名 |
| spring.cloud.enabled | 用于激活config自動(dòng)配置 |
| spring.cloud.zookeeper.config.root | zookeeper根路徑 |
| spring.cloud.zookeeper.profilSeparator | key分隔符 |
@Component
@ConfigurationProperties(prefix = "spring.datasources")
@Data
@RefreshScope
public class Db {
private String url;
}
系統(tǒng)中會(huì)讀取配置文件中的spring.datasources.url這個(gè)屬性值。結(jié)合我們的bootstrap.yml文件中。此時(shí)會(huì)去zookeeper系統(tǒng)中查找/config/cloud-payment-service,dev/spring.datasources.url這個(gè)值。
-關(guān)于那個(gè)zookeeper的key是如何來的。細(xì)心觀察下可以發(fā)現(xiàn)他的規(guī)律/${spring.cloud.zookeeper.root}/${spring.application.name}${spring.cloud.zookeeper.profileSeparator}${spring.profiles}/${實(shí)際的key}唯一注意的是需要在類上添加
@RefreshScope, 這個(gè)注解是cloud提供的。包括到后面的config配置中心都離不開這個(gè)注解此時(shí)通過前文提到的zkCli連接zk服務(wù),然后創(chuàng)建對(duì)應(yīng)節(jié)點(diǎn)就可以了。zookeeper服務(wù)需要逐層創(chuàng)建。比如上面提到的
/config/cloud-payment-services/spring.datasources.url,我們需要create /config然后create /config/cloud-payment-services在創(chuàng)建最后的內(nèi)容。我們可以提前在zk中創(chuàng)建好內(nèi)容。然后
localhost:8001/payment/getUrl獲取內(nèi)容。然后通過set /config/cloud-payment-services/spring.datasources.url helloworld在刷新接口就可以看到最新的helloworld了。這里不做演示。zookeeper原生的創(chuàng)建命令因?yàn)樾枰粚右粚觿?chuàng)建這還是很麻煩。還有我們有zkui這個(gè)插件。這個(gè)提供了zookeeper的可視化操作。還支持我們文件導(dǎo)入。上述的配置內(nèi)容我們只需要導(dǎo)入以下內(nèi)容的文件即可
/config/cloud-payment-services=spring.datasources.url=hello
zkui安裝使用
- 上面我們提到了一個(gè)工具
zkui,顧名思義他是zookeeper可視化工具。我們直接下載github源碼。

- 官網(wǎng)安裝步驟也很簡(jiǎn)單,因?yàn)樗褪且粋€(gè)jar服務(wù)。

- pom同級(jí)執(zhí)行maven clean install 打包jar 然后
nohup java -jar zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &后臺(tái)啟動(dòng)就行了。默認(rèn)端口9090 - 默認(rèn)用戶名密碼 官網(wǎng)都給了。 admin:manager


- 在jar包同級(jí)官網(wǎng)提供了一份zookeeper配置模板。在里面我們可以配置我們的zookeeper。 如果是集群就配置多個(gè)就行了。

關(guān)于這個(gè)zkui的使用這里不多介紹,就是一個(gè)可視化。程序員必備技能應(yīng)該都會(huì)使用的。
我們項(xiàng)目里使用的都是單機(jī)的zookeeper,但是在上面安裝的時(shí)候我們也有集群zookeeper。端口分別是1181,1182,1183. 我們?cè)趜kui的配置文件config.cfg中配置集群即可。


分布式鎖
分布式鎖常用在共享資源的獲取上。在分布式系統(tǒng)的中我們需要協(xié)調(diào)每個(gè)服務(wù)的調(diào)度。如果不進(jìn)行控制的話很大程度會(huì)造成資源的浪費(fèi)甚至是資源溢出。常見的就是我們的庫(kù)存。
- 之前我們springcloud中有一個(gè)order和兩個(gè)payment服務(wù)。payment主要用來做支付操作。如果訂單成功之后需要調(diào)用payment進(jìn)行扣款。這時(shí)候金額相當(dāng)于資源。這種資源對(duì)payment兩個(gè)服務(wù)來說是互斥操作。兩個(gè)payment過來操作金額時(shí)必須先后順序執(zhí)行。
mysql隔離控制
- 在以前分布式還不是很普及的時(shí)候我們正常處理這些操作時(shí)都是結(jié)束數(shù)據(jù)庫(kù)的事務(wù)隔離級(jí)別。
| 隔離級(jí)別 | 隔離級(jí)別 | 現(xiàn)象 |
|---|---|---|
| read uncommit | 讀未提交 | 產(chǎn)生臟讀 |
| read commited | 讀已提交 | 幻讀(insert、delete)、不可重復(fù)讀(update) |
| repeatable read | 可重復(fù)度 | 幻讀 |
| serializable | 串行化 | 無問題、效率變慢 |
- 基于mysql隔離級(jí)別我們可以設(shè)置數(shù)據(jù)庫(kù)為串行模式。但是帶來的問題是效率慢,所有的sql執(zhí)行都會(huì)串行。
mysql鎖
- 隔離級(jí)別雖然可以滿足但是帶來的問題確實(shí)不可接受。下面就會(huì)衍生出mysql鎖。我們可以單獨(dú)建一張表有數(shù)據(jù)代表上述成功。否則上鎖失敗。這樣我們?cè)诓僮鹘痤~扣減時(shí)先判斷下這張表有沒有數(shù)據(jù)進(jìn)行上鎖。但是如果上鎖之后會(huì)造成死鎖現(xiàn)象。因?yàn)槌绦虍惓_t遲沒有釋放鎖就會(huì)造成程序癱瘓。
- 這個(gè)時(shí)候我們可以定時(shí)任務(wù)清除鎖。這樣至少保證其他線程可用。
redis鎖
- 因?yàn)閞edis本身有失效屬性,我們不必?fù)?dān)心死鎖問題。且redis是內(nèi)存操作速度比mysql快很多。關(guān)于redis鎖的實(shí)現(xiàn)可以參考我的其他文章redis分布式鎖.
zookeeper鎖
- 因?yàn)閦ookeeper基于觀察者模式,我們上鎖失敗后可以監(jiān)聽對(duì)應(yīng)的值直到他失效時(shí)我們?cè)谶M(jìn)行我們的操作這樣能夠保證我們有序處理業(yè)務(wù),從而實(shí)現(xiàn)鎖的功能。

- 上圖是兩個(gè)線程上鎖的簡(jiǎn)易圖示。在zookeeper中實(shí)現(xiàn)分布式鎖主要依賴
CuratorFramework、InterProcessMutex兩個(gè)類
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.44.131:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex mutex = new InterProcessMutex(client, "/config/test");
long s = System.currentTimeMillis();
boolean acquire = mutex.acquire(100, TimeUnit.SECONDS);
if (acquire) {
long e = System.currentTimeMillis();
System.out.println(e - s+"@@@ms");
System.out.println("lock success....");
}
- 最終InterProcessMutex實(shí)現(xiàn)加鎖。加鎖會(huì)在指定的key上添加一個(gè)新的key且?guī)в芯幪?hào)。此時(shí)線程中的編號(hào)和獲取的/config/test下集合編號(hào)最小值相同的話則上鎖成功。T2則上鎖失敗,此時(shí)會(huì)想前一個(gè)序號(hào)的key添加監(jiān)聽。即當(dāng)00001失效時(shí)則00002對(duì)應(yīng)的T2就會(huì)獲取到鎖。這樣可以保證隊(duì)列的有序進(jìn)行。