什么是 zookeeper?

什么

1 zookeeper 與分布式系統(tǒng)

zookeeper 是一個(gè)中間件,為分布式系統(tǒng)提供協(xié)調(diào)(Coordination)服務(wù)瞒渠。是Google Chubby的開源實(shí)現(xiàn)嫩痰,Google的三篇論文總都提及了一個(gè)lock service -- Chubby,于是就有了Chubby的開源實(shí)現(xiàn) zookeeper。

1.1 什么是分布式系統(tǒng)

分布式系統(tǒng)

  • 很多臺(tái)計(jì)算機(jī)組成一個(gè)整體杆逗,一個(gè)整體一致對(duì)外并且處理同一個(gè)請(qǐng)求。

  • 內(nèi)部的每臺(tái)計(jì)算機(jī)都可以相互通信(rest/RPC)

  • 客戶端到服務(wù)端的一次請(qǐng)求,到響應(yīng)結(jié)束會(huì)歷經(jīng)多臺(tái)計(jì)算機(jī)

如下圖所示,小慕是客戶端,訪問分布式文件系統(tǒng)(網(wǎng)盤),服務(wù)端在服務(wù)器A,服務(wù)器B是服務(wù)器A的備用機(jī)從而實(shí)現(xiàn)高可用,具體的文件保存在文件服務(wù)器中耳贬,為了防止文件丟失,一個(gè)文件會(huì)保存在多個(gè)文件服務(wù)器中。一次請(qǐng)求歷經(jīng)了3臺(tái)計(jì)算機(jī)。

分布式系統(tǒng)圖解1

下圖是一個(gè)電商網(wǎng)站下單請(qǐng)求響應(yīng)流程育叁。用戶在商品頁面下單后,會(huì)經(jīng)過商品服務(wù)查看商品庫存,再經(jīng)過訂單服務(wù)生成訂單,再經(jīng)過賬單服務(wù),最后返回到商品頁面。一個(gè)請(qǐng)求歷經(jīng)了4臺(tái)計(jì)算機(jī)挖帘。

分布式系統(tǒng)圖解2

1.2 什么是zookeeper

zookeeper 是一個(gè)中間件骄崩,為分布式系統(tǒng)提供協(xié)調(diào)服務(wù)站楚。我們可以把zookeeper看成是一個(gè)分布式數(shù)據(jù)庫:

  • 一個(gè)具有文件系統(tǒng)特點(diǎn)的分布式數(shù)據(jù)庫
  • 解決了數(shù)據(jù)一致性問題的分布式數(shù)據(jù)庫
  • 具有發(fā)布訂閱功能的分布式數(shù)據(jù)庫

1.3 zookeeper的特性

  • 一致性:數(shù)據(jù)一致性彻况,數(shù)據(jù)按照順序分批入庫
  • 原子性:事務(wù)要么成功要么失敗
  • 單一視圖:客戶端連接集群中的zk節(jié)點(diǎn),數(shù)據(jù)都是一致的
  • 可靠性:每次對(duì)zk的操作狀態(tài)都會(huì)保存在服務(wù)端
  • 實(shí)時(shí)性:客戶端可以讀取到zk服務(wù)端的最新數(shù)據(jù)

2 zookeeper 的安裝與集群配置

  1. 安裝JDK左权,配置JAVA_HOME
  2. 官網(wǎng)下載zookeeper壓縮包,上傳到Linux機(jī)器 /opt 目錄
  3. 解壓,tar -zxvf zookeeper3.4.10.tar.gz cp -r /opt/zookeeper3.4.10.tar.gz /myzookeeper
  4. 修改zoo.cfg文件
  5. 啟動(dòng)zookeeper服務(wù)端突想,zkServer.sh start
  6. 檢查是否啟動(dòng)成功,ps -ef | grep zookeeper檢查進(jìn)程, echo ruok | nc 127.0.0.1:2181返回imok

查看官方文檔或zookeeper docs目錄的index.html夷蚊,Started Guide快速使用中介紹了單機(jī)安裝和一些zk的基本概念箱歧,Programmer's Guide詳細(xì)介紹了zk的數(shù)據(jù)模型,節(jié)點(diǎn)類型,會(huì)話病毡,Watch事件,ACL權(quán)限控制等。

zookeeper 目錄結(jié)構(gòu)

bin:主要的一些運(yùn)行命令膨更,zkCli.sh 是啟動(dòng)zk客戶端旦棉,zkServer.sh是啟動(dòng)zk服務(wù)端

conf:配置文件童本,我們需要修改zoo_sample.cfg

contrib:附加的一些功能

dist-maven:保存mvn編譯結(jié)果的目錄泵额,包括jar亡资,sources.jar母谎,pom.xml

docs:zk幫助文檔咬扇,可以打開index.html查看梭灿,與官網(wǎng)文檔相同

lib:開發(fā)時(shí)使用的jar包,

recipes:案例demo代碼,包括election休溶,lock孽尽,queue

src:zk源碼

zoo.cfg 配置

復(fù)制 conf 目錄下的zoo_sample.cfg熏挎,重命名為zoo.cfg哼勇。該配置文件中有以下幾個(gè)屬性:

  • tickTime:用于計(jì)算的時(shí)間單元帝璧,單位是毫秒。比如session超時(shí)設(shè)置為 N逐虚,則超時(shí)時(shí)間為N * tickTime

  • initLimit:用于集群聋溜,初始化連接時(shí)間叭爱。follower服務(wù)器啟動(dòng)過程中撮躁,需要連接并同步Leader節(jié)點(diǎn)的所有最新數(shù)據(jù),不能超過initLimit买雾,以tickTime的倍數(shù)來表示

  • syncLimit:用于集群把曼,限制了follower服務(wù)器與Leader服務(wù)器之間請(qǐng)求和應(yīng)答的時(shí)限(心跳機(jī)制)杨帽;如果A發(fā)出心跳包在syncLimit之后沒有收到B的響應(yīng),就認(rèn)為這個(gè)B已經(jīng)不在線了

  • dataDir:zookeeper存儲(chǔ)的數(shù)據(jù)文件目錄嗤军。dataDir=/usr/local/zookeeper/dataDir

  • dataLogDir:日志目錄注盈,如果不配置則使用dataDir。dataLogDir==/usr/local/zookeeper/dataLogDir

  • clientPort:客戶端連接服務(wù)器的端口叙赚,默認(rèn)2181

zk 的常用命令

zkServer.sh start 啟動(dòng)zk服務(wù)老客,在windows中是zkServer.cmd,不需要start命令震叮。

zkServer.sh stop 停止zk服務(wù)

zkServer.sh status 查看zookeeper狀態(tài)胧砰,返回zk的配置文件,客戶端連接端口苇瓣,服務(wù)器類型Mode為Leader或Follower

zkServer.sh status                                      
ZooKeeper JMX enabled by default
Using config: /opt/apache-zookeeper-3.5.5-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

echo ruok | nc 127.0.0.1:2181 返回imok說明zkServer啟動(dòng)成功

jps 查看啟動(dòng)的java進(jìn)程尉间,zk進(jìn)程名稱為QuorumPeerMain

zkCli.sh 啟動(dòng)zk客戶端,集群狀態(tài)需要制定zk服務(wù)器zkCli.sh -server 192.168.100.1:2181

zookeeper服務(wù)啟動(dòng)日志如下所示击罪,主要包括以下5部分內(nèi)容:

  1. 以單機(jī)模式啟動(dòng)running in standalone mode哲嘲,
  2. 讀取配置文件zoo.cfgReading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
  3. 開始啟動(dòng)服務(wù)Starting server媳禁,
  4. 顯示zk環(huán)境信息眠副,包括zk的版本號(hào) version,主機(jī)名稱 hostname损话,java 版本侦啸,java_home,classpath丧枪,操作系統(tǒng)光涂,用戶名稱等
  5. 顯示配置信息,包括tickTime set to 2000拧烦,綁定端口binding to port 0.0.0.0/0.0.0.0:2181
E:\zookeeper-3.4.10\bin>zkServer.cmd

E:\zookeeper-3.4.10\bin>call "C:\Program Files\Java\jdk1.8.0_51"\bin\java "-Dzookeeper.log.dir=E:\zookeeper-3.4.10\bin\.." "-Dzookeeper.root.logger=INFO,CONSOLE" -cp "E:\zookeeper-3.4.10\bin\..\build\classes;E:\zookeeper-3.4.10\bin\..\build\lib\*;E:\zookeeper-3.4.10\bin\..\*;E:\zookeeper-3.4.10\bin\..\lib\*;E:\zookeeper-3.4.10\bin\..\conf" org.apache.zookeeper.server.quorum.QuorumPeerMain "E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg"
2020-02-04 11:31:42,927 [myid:] - INFO  [main:QuorumPeerConfig@134] - Reading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
2020-02-04 11:31:42,938 [myid:] - INFO  [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3
2020-02-04 11:31:42,939 [myid:] - INFO  [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0
2020-02-04 11:31:42,940 [myid:] - INFO  [main:DatadirCleanupManager@101] - Purge task is not scheduled.
2020-02-04 11:31:42,943 [myid:] - WARN  [main:QuorumPeerMain@113] - Either no config or no quorum defined in config, running  in standalone mode
2020-02-04 11:31:43,037 [myid:] - INFO  [main:QuorumPeerConfig@134] - Reading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
2020-02-04 11:31:43,039 [myid:] - INFO  [main:ZooKeeperServerMain@96] - Starting server
2020-02-04 11:31:52,125 [myid:] - INFO  [main:Environment@100] - Server environment:zookeeper.version=3.4.10-39d3a4f269333c922ed3db283be479f9deacaa0f, built on 03/23/2017 10:13 GMT
2020-02-04 11:31:52,125 [myid:] - INFO  [main:Environment@100] - Server environment:host.name=DESKTOP-HSRU97J
2020-02-04 11:31:52,128 [myid:] - INFO  [main:Environment@100] - Server environment:java.version=1.8.0_51
2020-02-04 11:31:52,129 [myid:] - INFO  [main:Environment@100] - Server environment:java.vendor=Oracle Corporation
2020-02-04 11:31:52,130 [myid:] - INFO  [main:Environment@100] - Server environment:java.home=C:\Program Files\Java\jdk1.8.0_51\jre
2020-02-04 11:31:52,130 [myid:] - INFO  [main:Environment@100] - Server environment:java.class.path=E:\zookeeper-3.4.10\bin\..\build\classes;E:\zookeeper-3.4.10\bin\..\build\lib\*;E:\zookeeper-3.4.10\bin\..\zookeeper-3.4.10.jar;E:\zookeeper-3.4.10\bin\..\lib\jline-0.9.94.jar;E:\zookeeper-3.4.10\bin\..\lib\log4j-1.2.16.jar;E:\zookeeper-3.4.10\bin\..\lib\netty-3.10.5.Final.jar;E:\zookeeper-3.4.10\bin\..\lib\slf4j-api-1.6.1.jar;E:\zookeeper-3.4.10\bin\..\lib\slf4j-log4j12-1.6.1.jar;E:\zookeeper-3.4.10\bin\..\conf
2020-02-04 11:31:52,131 [myid:] - INFO  [main:Environment@100] - Server environment:java.library.path=C:\Program Files\Java\jdk1.8.0_51\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files\Java\jdk1.8.0_51\bin;C:\Program Files\Java\jdk1.8.0_51\jre\bin;E:\Program Files (x86)\apache-maven-3.3.9\bin;C:\WINDOWS\System32\OpenSSH\;E:\Program Files (x86)\Git\cmd;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\Users\Administrator\AppData\Local\GitHubDesktop\bin;%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;;.
2020-02-04 11:31:52,132 [myid:] - INFO  [main:Environment@100] - Server environment:java.io.tmpdir=C:\Users\ADMINI~1\AppData\Local\Temp\
2020-02-04 11:31:52,133 [myid:] - INFO  [main:Environment@100] - Server environment:java.compiler=<NA>
2020-02-04 11:31:52,138 [myid:] - INFO  [main:Environment@100] - Server environment:os.name=Windows 8.1
2020-02-04 11:31:52,139 [myid:] - INFO  [main:Environment@100] - Server environment:os.arch=amd64
2020-02-04 11:31:52,141 [myid:] - INFO  [main:Environment@100] - Server environment:os.version=6.3
2020-02-04 11:31:52,144 [myid:] - INFO  [main:Environment@100] - Server environment:user.name=Administrator
2020-02-04 11:31:52,145 [myid:] - INFO  [main:Environment@100] - Server environment:user.home=C:\Users\Administrator
2020-02-04 11:31:52,146 [myid:] - INFO  [main:Environment@100] - Server environment:user.dir=E:\zookeeper-3.4.10\bin
2020-02-04 11:31:52,158 [myid:] - INFO  [main:ZooKeeperServer@829] - tickTime set to 2000
2020-02-04 11:31:52,158 [myid:] - INFO  [main:ZooKeeperServer@838] - minSessionTimeout set to -1
2020-02-04 11:31:52,160 [myid:] - INFO  [main:ZooKeeperServer@847] - maxSessionTimeout set to -1
2020-02-04 11:31:52,301 [myid:] - INFO  [main:NIOServerCnxnFactory@89] - binding to port 0.0.0.0/0.0.0.0:2181
2020-02-04 11:32:01,829 [myid:] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /0:0:0:0:0:0:0:1:60100
2020-02-04 11:32:01,852 [myid:] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:ZooKeeperServer@942] - Client attempting to establish new session at /0:0:0:0:0:0:0:1:60100
2020-02-04 11:32:01,862 [myid:] - INFO  [SyncThread:0:FileTxnLog@203] - Creating new log file: log.7a8
2020-02-04 11:32:02,218 [myid:] - INFO  [SyncThread:0:ZooKeeperServer@687] - Established session 0x1700e411a110000 with negotiated timeout 30000 for client /0:0:0:0:0:0:0:1:60100

3 zookeeper基本數(shù)據(jù)模型

zookeeper數(shù)據(jù)模型是一個(gè)樹形結(jié)構(gòu)忘闻,類似于linux文件結(jié)構(gòu)。如下圖所示恋博,zk根目錄是 / 齐佳,是一個(gè)樹形結(jié)構(gòu)

zk結(jié)構(gòu)
zk結(jié)構(gòu)2
  • zk的數(shù)據(jù)模型可以理解為linux的文件目錄:/usr/local/...

  • 每一個(gè)節(jié)點(diǎn)都稱為znode,znode可以有子節(jié)點(diǎn)债沮,也可以有數(shù)據(jù)炼吴。

  • 每個(gè)節(jié)點(diǎn)分為臨時(shí)節(jié)點(diǎn)永久節(jié)點(diǎn),臨時(shí)節(jié)點(diǎn)在客戶端斷開后消失

  • 每個(gè)節(jié)點(diǎn)znode都有自己的版本號(hào)疫衩,可以通過命令行來顯示節(jié)點(diǎn)信息

  • 每當(dāng)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變化硅蹦,那么該節(jié)點(diǎn)的版本號(hào)會(huì)加1(樂觀鎖參考文檔1

  • 刪除/修改過時(shí)節(jié)點(diǎn)時(shí),因?yàn)榘姹咎?hào)不匹配,則會(huì)修改失斖邸(樂觀鎖參考文檔1

  • 每個(gè)節(jié)點(diǎn)znode存儲(chǔ)的數(shù)據(jù)不宜過大涮瞻,幾k即可

  • 節(jié)點(diǎn)可以設(shè)置權(quán)限控制列表acl,可以通過權(quán)限設(shè)置來限制用戶的訪問

3.1 zk 數(shù)據(jù)模型基本操作

客戶端連接

使用命令zkCli.sh啟動(dòng)客戶端假褪,啟動(dòng)成功信息如下署咽,表示連接到了 localhost:2181,連接狀態(tài)是CONNECTED生音,后面的數(shù)字 0 表示運(yùn)行的命令數(shù)

[zk: localhost:2181(CONNECTED) 0]

輸入help命令宁否,查看zk客戶端的常用命令如下,

ZooKeeper -server host:port cmd args
        stat path [watch]
        set path data [version]
        ls path [watch]
        delquota [-n|-b] path
        ls2 path [watch]
        setAcl path acl
        setquota -n|-b val path
        history
        redo cmdno
        printwatches on|off
        delete path [version]
        sync path
        listquota path
        rmr path
        get path [watch]
        create [-s] [-e] path data acl
        addauth scheme auth
        quit
        getAcl path
        close
        connect host:port

znode結(jié)構(gòu)

Znode由三部分組成:path缀遍,data家淤,Stat

[zk: ] get /zookeeper   # 節(jié)點(diǎn)路徑path
                # 節(jié)點(diǎn)保存的數(shù)據(jù)data,此節(jié)點(diǎn)數(shù)據(jù)為空
# 下面是Stat信息
cZxid = 0x0     # 節(jié)點(diǎn)創(chuàng)建操作的zxid,create
ctime = Thu Jan 01 08:00:00 CST 1970    # 創(chuàng)建節(jié)點(diǎn)時(shí)間
mZxid = 0x0     # 節(jié)點(diǎn)最新修改操作的zxid瑟由,modify 
mtime = Thu Jan 01 08:00:00 CST 1970    # 修改節(jié)點(diǎn)時(shí)間
pZxid = 0x0     # 子節(jié)點(diǎn)最后更新的zxid
cversion = -1   # 子節(jié)點(diǎn)的修改次數(shù),每次修改子節(jié)點(diǎn)version會(huì)加1, children  
dataVersion = 0 # 當(dāng)前節(jié)點(diǎn)保存的數(shù)據(jù)的修改次數(shù)冤寿,每次修改數(shù)據(jù)version會(huì)加1
aclVersion = 0  # 用戶控制權(quán)限的修改次數(shù)歹苦,每次修改權(quán)限version會(huì)加1
ephemeralOwner = 0x0    # 如果是臨時(shí)節(jié)點(diǎn)表示該節(jié)點(diǎn)的session id;非臨時(shí)節(jié)點(diǎn)則為0
dataLength = 0  # 節(jié)點(diǎn)保存的數(shù)據(jù)的大小
numChildren = 1 # 子節(jié)點(diǎn)的數(shù)量

3.2 zookeeper的應(yīng)用場景

  1. 統(tǒng)一配置文件管理督怜,即只需要部署一臺(tái)服務(wù)器殴瘦,則可以把相同的配置文件同步更新到其他所有服務(wù)器,此操作在云計(jì)算中應(yīng)用特別多号杠。<a href="#watchconfig">查看詳細(xì) </a>
  2. 服務(wù)注冊(cè)和發(fā)現(xiàn)蚪腋,類似消息隊(duì)列MQ,dubbo發(fā)布者會(huì)把數(shù)據(jù)存到znode中姨蟋,訂閱者會(huì)讀取這個(gè)數(shù)據(jù)屉凯。如下圖所示,發(fā)布者發(fā)布數(shù)據(jù)眼溶,訂閱者根據(jù)數(shù)據(jù)的變化進(jìn)行操作悠砚。利用 Znode 和 Watcher,可以實(shí)現(xiàn)分布式服務(wù)的注冊(cè)和發(fā)現(xiàn)堂飞,最著名的應(yīng)用就是阿里的分布式 RPC 框架 Dubbo灌旧。
    發(fā)布訂閱.png
  3. 提供分布式鎖,分布式環(huán)境中不同進(jìn)程之間會(huì)爭奪資源绰筛,類似多線程中的鎖枢泰。下圖中多個(gè)服務(wù)器中的進(jìn)程要操作網(wǎng)盤中的文件,為了避免沖突铝噩,需要分布式鎖衡蚂。雅虎研究員設(shè)計(jì) ZooKeeper 的初衷。利用 ZooKeeper 的臨時(shí)順序節(jié)點(diǎn),可以輕松實(shí)現(xiàn)分布式鎖讳窟。
    分布式鎖.png
  4. 集群管理让歼,集群中保證數(shù)據(jù)的強(qiáng)一致性。無論客戶端讀取哪一臺(tái)機(jī)器的數(shù)據(jù)丽啡,都會(huì)得到一致的數(shù)據(jù)谋右,因?yàn)閦ookeeper會(huì)將數(shù)據(jù)從主節(jié)點(diǎn)同步到其他節(jié)點(diǎn)
  5. 此外,Kafka补箍、HBase改执、Hadoop 也都依靠 ZooKeeper 同步節(jié)點(diǎn)信息,實(shí)現(xiàn)高可用坑雅。

4 zookeeper 基本特性與客戶端操作

4.1 session的基本原理

  1. 客戶端與服務(wù)端之間的的連接稱之為Session(會(huì)話)
  2. 每個(gè)Session會(huì)話都可以設(shè)置一個(gè)超時(shí)時(shí)間辈挂,超時(shí)后Session會(huì)被銷毀
  3. 心跳停止,則session過期
  4. Session過期裹粤,則臨時(shí)節(jié)點(diǎn)znode會(huì)被拋棄
  5. 心跳機(jī)制终蒂,客戶端向服務(wù)端的ping包請(qǐng)求,為了向服務(wù)端表示客戶端在線

4.2 常用命令行操作

zookeeper節(jié)點(diǎn)znode有許多狀態(tài)信息(Stat)遥诉,其中有兩個(gè)重要概念zxidversion numbers拇泣。

zxid

下面的命令中經(jīng)常出現(xiàn)Zxid,對(duì)ZooKeeper節(jié)點(diǎn)和子節(jié)點(diǎn)創(chuàng)建矮锈、更新數(shù)據(jù)(查詢不會(huì)修改zxid)都會(huì)收到一個(gè)zxid (ZooKeeper Transaction Id)形式的標(biāo)記霉翔。這將向ZooKeeper公開所有更改的總順序。每次更改都有一個(gè)惟一的zxid苞笨,如果zxid1小于zxid2债朵,則說明zxid1發(fā)生在zxid2之前。

zookeeper中的操作分為事務(wù)性操作(create瀑凝,set序芦,delete),會(huì)使得zxid加1猜丹,并且將該操作記錄持久化到日志中芝加;而非事務(wù)性操作(get,exist)不會(huì)修改zxid射窒。

Version Numbers

對(duì)節(jié)點(diǎn)的每次更改都會(huì)使得該節(jié)點(diǎn)的版本號(hào)version加 1藏杖。總共有三個(gè)version:

  1. version:對(duì)znode的數(shù)據(jù)的更改次數(shù)
  2. cversion:對(duì)znode的子節(jié)點(diǎn)的更改次數(shù)
  3. aclversion:對(duì)znode的ACL的更改次數(shù)

常用命令

ls:顯示指定目錄(節(jié)點(diǎn))下的子節(jié)點(diǎn)

ls2:ls2是顯示指定目錄(節(jié)點(diǎn))下的子節(jié)點(diǎn)脉顿,指定目錄的狀態(tài)信息蝌麸。等同于ls+stat命令

[zk: localhost:2181(CONNECTED) 2] ls /zookeeper
[quota]

[zk: localhost:2181(CONNECTED) 4] ls2 /zookeeper
[quota]
cZxid = 0x0     # 節(jié)點(diǎn)創(chuàng)建操作的zxid,create
ctime = Thu Jan 01 08:00:00 CST 1970    # 創(chuàng)建節(jié)點(diǎn)時(shí)間
mZxid = 0x0     # 節(jié)點(diǎn)最新修改操作的zxid艾疟,modify 
mtime = Thu Jan 01 08:00:00 CST 1970    # 修改節(jié)點(diǎn)時(shí)間
pZxid = 0x0     # 子節(jié)點(diǎn)最后更新的zxid
cversion = -1   # 子節(jié)點(diǎn)的修改次數(shù)来吩,每次修改子節(jié)點(diǎn)version會(huì)加1, children  
dataVersion = 0 # 當(dāng)前節(jié)點(diǎn)保存的數(shù)據(jù)的修改次數(shù)敢辩,每次修改數(shù)據(jù)version會(huì)加1
aclVersion = 0  # 用戶控制權(quán)限的修改次數(shù),每次修改權(quán)限version會(huì)加1
ephemeralOwner = 0x0    # 臨時(shí)節(jié)點(diǎn)擁有者,如果是臨時(shí)節(jié)點(diǎn)表示該節(jié)點(diǎn)的會(huì)話id弟疆;非臨時(shí)節(jié)點(diǎn)則為0
dataLength = 0  # 節(jié)點(diǎn)保存的數(shù)據(jù)的大小
numChildren = 1 # 子節(jié)點(diǎn)的數(shù)量

stat:顯示指定節(jié)點(diǎn)的狀態(tài)信息戚长,與get命令的區(qū)別是不顯示保存的數(shù)據(jù)信息

create [-s] [-e] path data acl:創(chuàng)建節(jié)點(diǎn),-e Ephemeral表示臨時(shí)節(jié)點(diǎn)怠苔,-s sequence表示順序節(jié)點(diǎn)同廉,data必填,否則無法創(chuàng)建柑司,不支持遞歸創(chuàng)建

get:獲取指定節(jié)點(diǎn)保存的信息和狀態(tài)信息

[zk:] create -e /imooc/tmp imooc-data2
Created /imooc/tmp

[zk: localhost:2181(CONNECTED) 9] get /imooc
imooc-data      # 節(jié)點(diǎn)保存的數(shù)據(jù)信息
cZxid = 0x7ab
ctime = Tue Feb 04 16:11:01 CST 2020
pZxid = 0x7ac   # 子節(jié)點(diǎn)最新操作的zxid
cversion = 1    # 子節(jié)點(diǎn)的修改次數(shù)
ephemeralOwner = 0x0    # 非臨時(shí)節(jié)點(diǎn),所以為0
numChildren = 1         # 創(chuàng)建了1個(gè)子節(jié)點(diǎn),所以為1

[zk: localhost:2181(CONNECTED) 10] get /imooc/tmp
imooc-data2 
cZxid = 0x7ac   # 節(jié)點(diǎn)創(chuàng)建操作的zxid,與父節(jié)點(diǎn)的pZxid相同
ephemeralOwner = 0x1700e411a110001      # 臨時(shí)節(jié)點(diǎn),表示會(huì)話id

問題:創(chuàng)建臨時(shí)節(jié)點(diǎn)后迫肖,停止客戶端,該臨時(shí)節(jié)點(diǎn)會(huì)立即消失嗎攒驰?

使用客戶端A創(chuàng)建臨時(shí)節(jié)點(diǎn)ephNode蟆湖,客戶端B可以查看該臨時(shí)節(jié)點(diǎn),強(qiáng)行終止客戶端A(不能使用quit命令退出)玻粪,發(fā)現(xiàn)客戶端B仍然能夠查看該臨時(shí)節(jié)點(diǎn)隅津,因?yàn)樾奶嬖诔瑫r(shí)時(shí)間,在超時(shí)范圍內(nèi)劲室,zk認(rèn)為該客戶端仍然正常饥瓷。

當(dāng)心跳超時(shí)后,session會(huì)話過期痹籍,臨時(shí)節(jié)點(diǎn)ephNode 也會(huì)被拋棄,此時(shí)使用客戶端B就查看不到該臨時(shí)節(jié)點(diǎn)了晦鞋。查看zoo.cfg文件蹲缠,syncLimit屬性就是心跳超時(shí)時(shí)間

# create -s 表示創(chuàng)建序列自增節(jié)點(diǎn),設(shè)置的節(jié)點(diǎn)名稱后會(huì)添加自增數(shù)
[zk:] create -s /imooc/seq seq-data
Created /imooc/seq0000000005
[zk:] create -s /imooc/seque seq-data
Created /imooc/seque0000000006

set path data [version]:設(shè)置節(jié)點(diǎn)的數(shù)據(jù) ,version表示修改指定dataversion的數(shù)據(jù)悠垛,如果參數(shù)version與節(jié)點(diǎn)的dataversion不一致线定,則修改失敗,這是為了避免多個(gè)客戶端同時(shí)修改數(shù)據(jù)競爭產(chǎn)生的問題确买。

[zk: localhost:2181(CONNECTED) 3] get /imooc
imooc-data
mZxid = 0x7ab
dataVersion = 0

[zk: localhost:2181(CONNECTED) 4] set /imooc new-data
mZxid = 0x7c0       # 修改了節(jié)點(diǎn)數(shù)據(jù),記錄修改操作的zxid
dataVersion = 1     # 修改節(jié)點(diǎn)數(shù)據(jù)的次數(shù)

#修改節(jié)點(diǎn)指定版本的數(shù)據(jù)
[zk: localhost:2181(CONNECTED) 6] set /imooc 123 1
dataVersion = 2

# 當(dāng)節(jié)點(diǎn)dataVersion與參數(shù)1不相等時(shí),則修改失敗.樂觀鎖
[zk: localhost:2181(CONNECTED) 7] set /imooc 123 1
version No is not valid : /imooc

delete path [version]:刪除節(jié)點(diǎn)斤讥,version需要與節(jié)點(diǎn)dataversion一致,否則刪除失敗

4.3 watcher機(jī)制

客戶端可以在節(jié)點(diǎn)znode上設(shè)置一個(gè)watch事件湾趾,對(duì)該znode的更改將觸發(fā)該watch事件芭商,并清除該watch事件。當(dāng)一個(gè)watch事件觸發(fā)時(shí)搀缠,zookeeper會(huì)向客戶端發(fā)送一個(gè)通知铛楣。watcher機(jī)制的特點(diǎn)如下所示:

  • 針對(duì)每個(gè)節(jié)點(diǎn)znode的操作,都會(huì)有一個(gè)監(jiān)督者watcher

  • 當(dāng)監(jiān)控的某個(gè)節(jié)點(diǎn)znode發(fā)生了變化艺普,則觸發(fā)watcher事件(類似觸發(fā)器)

  • watcher是一次性的簸州,觸發(fā)后以及銷毀

  • 節(jié)點(diǎn)znode自己鉴竭、子孫節(jié)點(diǎn)的創(chuàng)建、刪除岸浑、數(shù)據(jù)修改都能觸發(fā)當(dāng)前節(jié)點(diǎn)的watcher搏存。節(jié)點(diǎn)沒有創(chuàng)建之前也能添加watcher

  • 不同類型的操作,觸發(fā)不同的watcher事件矢洲,包括節(jié)點(diǎn)創(chuàng)建璧眠、刪除、數(shù)據(jù)修改事件

    • 創(chuàng)建自身節(jié)點(diǎn)觸發(fā):NodeCreated

    • 修改自身節(jié)點(diǎn)數(shù)據(jù)觸發(fā):NodeDataChanged

    • 刪除自身節(jié)點(diǎn)觸發(fā):NodeDeleted

    • 創(chuàng)建兵钮、刪除子節(jié)點(diǎn)都會(huì)觸發(fā):NodeChildrenChanged

    • 修改子節(jié)點(diǎn)數(shù)據(jù)不會(huì)觸發(fā)watch事件

stat path [watch]:獲取節(jié)點(diǎn)狀態(tài)信息蛆橡,給節(jié)點(diǎn)添加一次性的watch事件

get path [watch]:獲取節(jié)點(diǎn)狀態(tài)信息和數(shù)據(jù)信息,給節(jié)點(diǎn)添加一次性的watch事件

ls2 path [watch]:獲取節(jié)點(diǎn)狀態(tài)信息和子節(jié)點(diǎn)掘譬,給節(jié)點(diǎn)添加一次性的watch事件

下面代碼演示給節(jié)點(diǎn)mywatch添加watch事件泰演,創(chuàng)建、刪除葱轩、數(shù)據(jù)修改節(jié)點(diǎn)mywatch自己會(huì)觸發(fā)哪些類型的watch事件:

# 給不存在的節(jié)點(diǎn)mywatch添加watch事件
[zk: localhost:2181(CONNECTED) 14] stat /mywatch watch
Node does not exist: /mywatch
# 創(chuàng)建節(jié)點(diǎn)mywatch睦焕,觸發(fā)watch事件WatchedEvent,類型是NodeCreated
[zk: localhost:2181(CONNECTED) 15] create /mywatch 123

WATCHER::
Created /mywatch
WatchedEvent state:SyncConnected type:NodeCreated path:/mywatch

# 因?yàn)閣atch事件是一次性的靴拱,所以我們重新添加watch事件
[zk: localhost:2181(CONNECTED) 19] get /mywatch watch
123
# 修改節(jié)點(diǎn)數(shù)據(jù)垃喊,觸發(fā)WatchedEvent,類型是NodeDataChanged
[zk: localhost:2181(CONNECTED) 20] set /mywatch 456

WATCHER::ctime = Thu Feb 06 12:22:27 CST 2020
WatchedEvent state:SyncConnected type:NodeDataChanged path:/mywatchmZxid = 0x7c9

# 再次添加watch事件
[zk: localhost:2181(CONNECTED) 21] get /mywatch watch
456

# 刪除節(jié)點(diǎn)mywatch,觸發(fā)WatchedEvent,類型是NodeDeleted
[zk: localhost:2181(CONNECTED) 22] delete /mywatch

WATCHER::
[zk: localhost:2181(CONNECTED) 23]
WatchedEvent state:SyncConnected type:NodeDeleted path:/mywatch

創(chuàng)建袜炕、刪除子節(jié)點(diǎn)會(huì)觸發(fā)NodeChildrenChanged事件本谜,但修改子節(jié)點(diǎn)數(shù)據(jù)不會(huì)觸發(fā)watch事件

# 創(chuàng)建節(jié)點(diǎn)mywatch
[zk: localhost:2181(CONNECTED) 31] create /mywatch 123
Created /mywatch
# 給節(jié)點(diǎn)添加watch事件
[zk: localhost:2181(CONNECTED) 33] ls /mywatch watch
[]
# 創(chuàng)建mywatch的子節(jié)點(diǎn),觸發(fā)WatchedEvent,類型是NodeChildrenChanged
[zk: localhost:2181(CONNECTED) 34] create /mywatch/cnode 666

WATCHER::Created /mywatch/cnode    # 這里是創(chuàng)建的節(jié)點(diǎn)路徑
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mywatch     # 這里是觸發(fā)watch事件的節(jié)點(diǎn)自身

watcher是當(dāng)前客戶端加在節(jié)點(diǎn)znode上的觸發(fā)器,

watcher使用場景

  • 統(tǒng)一配置文件管理偎窘。sqlConfig節(jié)點(diǎn)保存json數(shù)據(jù)乌助,即對(duì)配置文件的操作和文件路徑,某個(gè)客戶端對(duì)sqlConfig節(jié)點(diǎn)添加了watch事件陌知,當(dāng)節(jié)點(diǎn)數(shù)據(jù)更新后他托,所有客戶端都能監(jiān)聽到,然后根據(jù)節(jié)點(diǎn)數(shù)據(jù)更新本地配置信息仆葡。<a href="#watchconfig">第7.2章節(jié) </a>詳細(xì)介紹了利用Watch實(shí)現(xiàn)統(tǒng)一配置文件管理赏参。
統(tǒng)一配置信息.png

4.4 ACL權(quán)限控制

ACL(access control lists)

  • 針對(duì)節(jié)點(diǎn)可以設(shè)置相關(guān)讀寫權(quán)限,保障數(shù)據(jù)安全性
  • 權(quán)限permissions可以指定不同的權(quán)限范圍和角色
  • zk的acl通過[schema: id : permissions] 來構(gòu)成權(quán)限列表沿盅;
    • schema:權(quán)限機(jī)制把篓,有五種類型:world,auth腰涧,digest纸俭,ip,super
    • permissions:創(chuàng)建南窗、刪除揍很、讀郎楼、寫權(quán)限
    • id:用戶,permissions:權(quán)限組合字符串

身份認(rèn)證的5種類型schema

world:默認(rèn)方式窒悔,相當(dāng)于全世界都能訪問呜袁,只有一個(gè)id anyone world:anyone:[permissions]

auth:代表節(jié)點(diǎn)授權(quán)的用戶 auth:username:password:cdrwa

digest:即用戶名:密碼這種方式認(rèn)證简珠,這也是業(yè)務(wù)系統(tǒng)中最常用的阶界,digest:username:BSE64(SHA1(password)):[permissions]

ip:指定的ip地址才可以訪問,ip:182.168.1.1:[permissions]

super:超級(jí)管理員聋庵,擁有所有權(quán)限膘融,需要修改zkServer.sh文件

permissions

權(quán)限字符串縮寫crdwa

  • create:創(chuàng)建子節(jié)點(diǎn)
  • read:獲取節(jié)點(diǎn) / 子節(jié)點(diǎn)信息和數(shù)據(jù)
  • delete:刪除子節(jié)點(diǎn)
  • write:設(shè)置節(jié)點(diǎn)數(shù)據(jù)
  • admin:管理權(quán)限,設(shè)置節(jié)點(diǎn)ACL的權(quán)限

訪問

addauth digest user:pwd 來添加當(dāng)前上下文中的授權(quán)用戶祭玉,authdigest兩種授權(quán)方式均可以通過addauth digest user:pwd命令(明文密碼)訪問氧映。

登錄后設(shè)置權(quán)限可省略u(píng)sername和password

# 未添加授權(quán)addauth, 設(shè)置ACL失敗
[zk: localhost:2181(CONNECTED) 7] setAcl /myacl auth:mao:mao:crdwa
Acl is not valid : /myacl
# 添加授權(quán)用戶mao:mao
[zk: localhost:2181(CONNECTED) 8] addauth digest mao:mao
[zk: localhost:2181(CONNECTED) 9] setAcl /myacl auth:mao:mao:crwa
aclVersion = 1
[zk: localhost:2181(CONNECTED) 12] setAcl /myacl auth:mao:123456:crdwa
aclVersion = 2

# ACL列表仍然只有 mao:mao, 沒有mao:123456
[zk: localhost:2181(CONNECTED) 13] getAcl /myacl
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: crwa
# 省略u(píng)sername和password, 使用當(dāng)前授權(quán)的用戶, 修改權(quán)限為crdwa
[zk: localhost:2181(CONNECTED) 14] setAcl /myacl auth:::crdwa
aclVersion = 3
# 修改成功
[zk: localhost:2181(CONNECTED) 29] getAcl /myacl
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdrwa

ACL命令行

  • getAcl:獲取某個(gè)節(jié)點(diǎn)的acl權(quán)限信息,getAcl /imooc/myauth
  • setAcl:設(shè)置某個(gè)節(jié)點(diǎn)的acl權(quán)限信息脱货,setAcl /imooc/myauth auth:mao:mao:cdrwa
  • addauth: 來添加當(dāng)前上下文中的授權(quán)用戶岛都,addauth digest mao:maos
# 修改myauth節(jié)點(diǎn)的acl權(quán)限為crwa,即無法刪除子節(jié)點(diǎn)
[zk: localhost:2181(CONNECTED) 26] setAcl /imooc/myauth world:anyone:crwa
aclVersion = 1
# 創(chuàng)建myauth的子節(jié)點(diǎn)test
[zk: localhost:2181(CONNECTED) 27] create /imooc/myauth/test 222
Created /imooc/myauth/test
# 刪除myauth子節(jié)點(diǎn)test,發(fā)生權(quán)限錯(cuò)誤
[zk: localhost:2181(CONNECTED) 28] delete /imooc/myauth/test
Authentication is not valid : /imooc/myauth/test

# 添加當(dāng)前上下文中的授權(quán)用戶,相當(dāng)于登錄振峻,否則下面的setAcl命令會(huì)失敗
[zk: localhost:2181(CONNECTED) 31] addauth digest mao:mao
# 使用用戶mao設(shè)置acl權(quán)限, 當(dāng)用戶名密碼不是當(dāng)前用戶mao:mao時(shí)不生效
# 和下行命令等價(jià) setAcl /imooc/myauth auth:::cdrwa
[zk: localhost:2181(CONNECTED) 32] setAcl /imooc/myauth auth:mao:mao:cdrwa
aclVersion = 2
# 查看myauth權(quán)限
[zk: localhost:2181(CONNECTED) 33] getAcl /imooc/myauth
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdrwa
[zk: localhost:2181(CONNECTED) 34]

# 使用digest設(shè)置acl權(quán)限
[zk: localhost:2181(CONNECTED) 34] setAcl /imooc/myauth digest:mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=:cdra
aclVersion = 3
# 查看myauth權(quán)限,發(fā)現(xiàn)已修改
[zk: localhost:2181(CONNECTED) 35] getAcl /imooc/myauth
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdra
# 修改節(jié)點(diǎn)數(shù)據(jù),提示權(quán)限不合法
[zk: localhost:2181(CONNECTED) 36] set /imooc/myauth 222
Authentication is not valid : /imooc/myauth

super 權(quán)限設(shè)置

修改zkServer.sh文件臼疫,添加系統(tǒng)屬性“-Dzookeeper.DigestAuthenticationProvider.superDigest=username:BASE64(SHA1(password))”。zk會(huì)讀取該屬性并設(shè)置為super用戶扣孟,源碼如下圖所示

image.png

4.5 四字命令

四字命令是在Linux中使用(zkCli無法使用)來zookeeper服務(wù)的當(dāng)前狀態(tài)及相關(guān)信息的烫堤,四字命令的

zk可以通過它自身提供的簡寫命令來和服務(wù)器交互,需要使用到nc命令凤价,需要使用yum install nc安裝塔逃,命令格式為 echo [commond] | nc [ip] [port]

  • stat:查看zk的狀態(tài)信息和Mode類型
  • ruok:查看當(dāng)前zkServer是否啟動(dòng),若啟動(dòng)成功則返回 imok
  • dump:列出未經(jīng)處理的會(huì)話和臨時(shí)節(jié)點(diǎn)
  • conf:查看服務(wù)配置
  • cons:展示連接到服務(wù)器的客戶端信息
  • envi:環(huán)境變量
  • mntr:監(jiān)控zk健康信息
  • wchs:展示watch的詳細(xì)信息
  • wchc:通過session列出服務(wù)器watch的詳細(xì)信息料仗,
  • wchp:通過路徑列出服務(wù)器 watch的詳細(xì)信息

5 zookeeper集群安裝

6 zookeeper JavaAPI開發(fā)客戶端

  1. 依賴

    使用zookeeper 原生JavaAPI開發(fā)需要引入相應(yīng)的jar包,依賴pom.xml如下所示:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.7</version>
</dependency>

6.1 會(huì)話連接與恢復(fù)(源碼)

建立客戶端與zk服務(wù)端的session連接伏蚊,需要以下三步:

  1. 需要?jiǎng)?chuàng)建zk對(duì)象立轧,傳入Watcher對(duì)象
  2. 啟動(dòng)sendThread線程與zk服務(wù)端建立連接
  3. 啟動(dòng)eventThread線程,不斷檢查連接是否建立成功躏吊;若成功氛改,則觸發(fā)watch事件,使用Watcher發(fā)送watch通知比伏。
// ZKConnect是Watcher接口的實(shí)現(xiàn)類胜卤,用于發(fā)送watch通知,即調(diào)用Watch.process()方法
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnect());

客戶端連接zk服務(wù)端代碼如下所示:

// 實(shí)現(xiàn)Watcher接口,用于通知客戶端是否連接成功
public class ZKConnect implements Watcher {

   final static Logger log = LoggerFactory.getLogger(ZKConnect.class);

   public static final String zkServerPath = "localhost:2181";
   //  public static final String zkServerPath = "192.168.1.111:2181,192.168.1.111:2182,192.168.1.111:2183";
   public static final Integer timeout = 5000;

   public static void main(String[] args) throws Exception {
       /**
        * 客戶端和zk服務(wù)端鏈接是一個(gè)異步的過程
        * 當(dāng)連接成功后后赁项,客戶端會(huì)收的一個(gè)watch通知葛躏,即調(diào)用Watch.process()方法
        *
        * 參數(shù):
        * connectString:連接服務(wù)器的ip字符串澈段,多個(gè)ip用逗號(hào)分隔
        * sessionTimeout:超時(shí)時(shí)間,心跳收不到了舰攒,那就超時(shí)
        * watcher:通知事件败富,如果有對(duì)應(yīng)的事件觸發(fā),則會(huì)收到一個(gè)通知摩窃;如果不需要兽叮,那就設(shè)置為null
        * sessionId:會(huì)話的id
        * sessionPasswd:會(huì)話密碼   當(dāng)會(huì)話丟失后,可以依據(jù) sessionId 和 sessionPasswd 重新獲取會(huì)話
        */
       ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnect());

       log.warn("客戶端開始連接zookeeper服務(wù)器...");
       log.warn("連接狀態(tài):{}", zk.getState());

       // 等待連接線程執(zhí)行完畢
       new Thread().sleep(2000);

       log.warn("連接狀態(tài):{}", zk.getState());
   }

   // 連接成功后使用watch事件進(jìn)行通知
   @Override
   public void process(WatchedEvent event) {
       log.warn("接受到watch通知:{}", event);
   }
}

會(huì)話恢復(fù)

將之前創(chuàng)建的zk連接會(huì)話的sessionIdsessionPasswd保存猾愿,然后利用其創(chuàng)建新的

zk對(duì)象即可恢復(fù)會(huì)話鹦聪,查看完整代碼

    ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnectSessionWatcher());
        
        long sessionId = zk.getSessionId();
        byte[] sessionPassword = zk.getSessionPasswd();
        
        log.warn("客戶端開始連接zookeeper服務(wù)器...");
        log.warn("連接狀態(tài):{}", zk.getState());
        new Thread().sleep(1000);
        log.warn("連接狀態(tài):{}", zk.getState());
        
        new Thread().sleep(200);
        
        // 開始會(huì)話重連,使用之前保存的sessionId和password創(chuàng)建新的連接
        ZooKeeper zkSession = new ZooKeeper(zkServerPath, 
                                            timeout, 
                                            new ZKConnectSessionWatcher(), 
                                            sessionId, 
                                            sessionPassword);
    

6.2 節(jié)點(diǎn)增刪改查

創(chuàng)建節(jié)點(diǎn)有同步、異步兩種形式蒂秘,是重載的create方法:

  1. 同步創(chuàng)建有返回值泽本,成功返回節(jié)點(diǎn)路徑,失敗拋出異常KeeperException

  2. 異步創(chuàng)建無返回值材彪,成功調(diào)用參數(shù)中的回調(diào)方法StringCallback.processResult()观挎,方法內(nèi)容可以自己實(shí)現(xiàn),也可以根據(jù)ctx執(zhí)行不同的操作

  3. 都不支持節(jié)點(diǎn)的遞歸創(chuàng)建

// 同步創(chuàng)建段化,path,data,acl與命令create一致, createmode是-s序列 -e臨時(shí)節(jié)點(diǎn)的結(jié)合體
public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) throws KeeperException, InterruptedException

// 異步創(chuàng)建嘁捷,StringCallback是創(chuàng)建成功后的回調(diào)函數(shù), ctx是成功后的返回信息,一般為json
public void create(final String path, byte data[], List<ACL> acl, CreateMode createMode,  StringCallback cb, Object ctx)

同步創(chuàng)建節(jié)點(diǎn)

    // 如果創(chuàng)建失敗會(huì)拋出異常KeeperException
    @Test
    public void createNode() throws KeeperException, InterruptedException {
        /**
         * 同步或者異步創(chuàng)建節(jié)點(diǎn),都不支持子節(jié)點(diǎn)的遞歸創(chuàng)建显熏,異步有一個(gè)callback函數(shù)
         * 參數(shù):
         * path:創(chuàng)建的路徑
         * data:存儲(chǔ)的數(shù)據(jù)的byte[]
         * acl:控制權(quán)限策略
         *          Ids.OPEN_ACL_UNSAFE --> world:anyone:cdrwa
         *          CREATOR_ALL_ACL --> auth:user:password:cdrwa
         * createMode:節(jié)點(diǎn)類型, 是一個(gè)枚舉
         *          PERSISTENT:持久節(jié)點(diǎn)
         *          PERSISTENT_SEQUENTIAL:持久順序節(jié)點(diǎn)
         *          EPHEMERAL:臨時(shí)節(jié)點(diǎn)
         *          EPHEMERAL_SEQUENTIAL:臨時(shí)順序節(jié)點(diǎn)
         */
        String result = zookeeper.create("/testnode", "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        log.warn("創(chuàng)建" + result + "成功");
    }

異步創(chuàng)建節(jié)點(diǎn)

    @Test
    public void createNodeAsync() throws InterruptedException {
        String ctx = "{'create':'success'}";

        // 因?yàn)槭钱惒?創(chuàng)建成功后調(diào)用StringCallback.processResult()
        zookeeper.create("/testnode3/abc", "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, String name) {
                System.out.println("創(chuàng)建節(jié)點(diǎn): " + path);
                System.out.println((String)ctx);
            }
        }, ctx);
        Thread.sleep(2000);
    }

設(shè)置節(jié)點(diǎn)數(shù)據(jù)

    // 版本號(hào)錯(cuò)誤會(huì)拋出KeeperException: Badversion for /node
    @Test
    public void setData() throws KeeperException, InterruptedException {
        /**
         * 參數(shù):
         * path:節(jié)點(diǎn)路徑
         * data:數(shù)據(jù)
         * version:數(shù)據(jù)版本
         * 返回值Stat等價(jià)于stat命令, 返回節(jié)點(diǎn)狀態(tài)信息
         */
        Stat status = zookeeper.setData("/testnode", "666".getBytes(), 1);
        System.out.println(status.getVersion());
    }

刪除節(jié)點(diǎn)

    @Test
    public void deleteNodeAsync() throws KeeperException, InterruptedException {
        String result = "{'delete':'success'}";
        zookeeper.delete("/testnode", 2, new AsyncCallback.VoidCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx) {
                System.out.println("刪除節(jié)點(diǎn)" + path);
                System.out.println((String)ctx);
            }
        }, result);
    }

查詢節(jié)點(diǎn)數(shù)據(jù)

    /**
     * 獲取節(jié)點(diǎn)數(shù)據(jù), 等價(jià)于命令 get path [watch]
     * Stat保存節(jié)點(diǎn)狀態(tài)信息, data保存節(jié)點(diǎn)數(shù)據(jù)
     * watch=false表示不添加監(jiān)聽,為true表示添加監(jiān)聽,監(jiān)聽事件在watch的`process`中觸發(fā)
     */
    @Test
    public void getNodeData() throws KeeperException, InterruptedException {
        Stat status = new Stat();
        byte[] data = zookeeper.getData("/imooc", false, status);
        System.out.println("節(jié)點(diǎn)數(shù)據(jù):" + new String(data));
    }

獲取子節(jié)點(diǎn)列表

    /**
     * 獲取子節(jié)點(diǎn)列表
     * stat用于獲得當(dāng)前節(jié)點(diǎn)狀態(tài)信息
     */
    @Test
    public void getChildrenNode() throws KeeperException, InterruptedException {
        Stat status = new Stat();
        List<String> children = zookeeper.getChildren("/imooc", false, status);
        children.forEach(e -> System.out.println(e));
    }

判斷節(jié)點(diǎn)是否存在

    @Test
    public void nodeExist() throws KeeperException, InterruptedException {
        Stat status = zookeeper.exists("/imooc", false);
        if(status == null) {
            System.out.println("當(dāng)前節(jié)點(diǎn)不存在");
        }else {
            System.out.println("當(dāng)前節(jié)點(diǎn)存在雄嚣,dataVersion:" + status.getVersion());
        }
    }

6.3 watch與acl

7 Apache Curator

Apache Curator也是一款開源的zookeeper客戶端Java API,企業(yè)常用于操作zookeeper喘蟆。API簡單易用营密,提供常用的工具類,提供了分布式鎖解決方案臀突,并且解決了原生API的三個(gè)問題:

  1. 超時(shí)重連贸营,需要手動(dòng)重連
  2. watch注冊(cè)后,一次觸發(fā)就會(huì)失效
  3. 不支持遞歸創(chuàng)建節(jié)點(diǎn)

7.1 使用Curator操作zk

查看完整代碼

  1. 引入依賴橙弱,pom.xml需要引入Curator依賴
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.0.0</version>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
</dependency>
  1. 創(chuàng)建客戶端歧寺,設(shè)置重試策略
    /**
    * 同步創(chuàng)建zk示例,原生api是異步的
    *
    * curator鏈接zookeeper的策略:ExponentialBackoffRetry
    * baseSleepTimeMs:初始sleep的時(shí)間
    * maxRetries:最大重試次數(shù)
    * maxSleepMs:最大重試時(shí)間
    */
    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    /**
    * 獲取zk客戶端棘脐,需要傳入zk地址斜筐,超時(shí)時(shí)間,重試策略蛀缝,命名空間
    * namespace,該客戶端即所有增刪改查操作的節(jié)點(diǎn)路徑前面都會(huì)加上 /workspace
    */
    client = CuratorFrameworkFactory.builder()
        .connectString(zkServerPath)
        .sessionTimeoutMs(10000).retryPolicy(retryPolicy)
        .namespace("workspace").build();
    client.start();
  1. 檢查客戶端連接狀態(tài)顷链,是否啟動(dòng),測試關(guān)閉客戶端
    /**
     * 獲取客戶端的連接狀態(tài), 關(guān)閉會(huì)話連接
     * 用于替代過時(shí)方法isStarted()
     */
    @Test
    public void getzkStatus() throws InterruptedException {
        boolean isZkCuratorStarted = client.getState() == CuratorFrameworkState.STARTED;
        System.out.println("當(dāng)前客戶的狀態(tài):" + (isZkCuratorStarted ? "連接中" : "已關(guān)閉"));
        Thread.sleep(3000);

        client.close();
        boolean isZkCuratorStarted2 = client.getState() == CuratorFrameworkState.STARTED;
        System.out.println("當(dāng)前客戶的狀態(tài):" + (isZkCuratorStarted2 ? "連接中" : "已關(guān)閉"));
    }
  1. 操作節(jié)點(diǎn)
    1. 創(chuàng)建節(jié)點(diǎn)屈梁、
    2. 刪除節(jié)點(diǎn)嗤练、
    3. 設(shè)置節(jié)點(diǎn)數(shù)據(jù)榛了、
    4. 獲取節(jié)點(diǎn)數(shù)據(jù)和狀態(tài)信息、
    5. 獲取子節(jié)點(diǎn)列表潭苞,
    6. 判斷節(jié)點(diǎn)是否存在
    /**
     * 創(chuàng)建節(jié)點(diǎn)
     * <p>
     * client的命名空間是/workspace, 即所有增刪改查的操作的節(jié)點(diǎn)路徑前面都會(huì)加上 /workspace
     * 會(huì)在第一次創(chuàng)建節(jié)點(diǎn)時(shí)自動(dòng)創(chuàng)建父節(jié)點(diǎn)/workspace
     * 如果節(jié)點(diǎn)已存在拋出異常KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /workspace/curator/imooc
     */
    @Test
    public void createNode() throws Exception {
        byte[] data = "abc".getBytes();
        String nodePath = "/curator/imooc";
        String path = client.create().creatingParentsIfNeeded()       // 如果父節(jié)點(diǎn)不存在,創(chuàng)建父節(jié)點(diǎn)
                .withMode(CreateMode.PERSISTENT)        // 設(shè)置節(jié)點(diǎn)-s -t 臨時(shí),序列界定啊
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)   // 設(shè)置用戶控制權(quán)限
                .forPath(nodePath, data);

        System.out.println(path + "節(jié)點(diǎn)創(chuàng)建成功");
    }

    /**
     * 設(shè)置節(jié)點(diǎn)數(shù)據(jù), 可設(shè)置dataVersion
     * @throws Exception
     */
    @Test
    public void setData() throws Exception {
        byte[] data = "123".getBytes();
        String nodePath = "/curator/imooc";

        Stat stat = client.setData()
//                .withVersion(2)   // 設(shè)置版本號(hào),可省略, 若版本號(hào)錯(cuò)誤拋出異常
                .forPath(nodePath, data);
        System.out.println("dataVersion" + stat.getVersion());
    }

    /**
     * 刪除節(jié)點(diǎn), 版本號(hào)可省略
     * 如果節(jié)點(diǎn)不存在會(huì)拋出異常KeeperException$NoNodeException: KeeperErrorCode = NoNode for /workspace/curator
     */
    @Test
    public void deleteNode() throws Exception {
        String nodePath = "/curator";
        client.delete()
                .guaranteed()       // 如果刪除失敗,那么后端會(huì)繼續(xù)刪除,直至成功
                .deletingChildrenIfNeeded()     // 如果存在子節(jié)點(diǎn),就刪
                //  .withVersion(2)
                .forPath(nodePath);
    }

    /**
     * 獲取節(jié)點(diǎn)數(shù)據(jù)和狀態(tài)信息
     * 狀態(tài)信息保存在Stat中, 數(shù)據(jù)保存在data中
     *
     * @throws Exception
     */
    @Test
    public void getNode() throws Exception {
        String nodePath = "/curator/imooc";
        Stat stat = new Stat();

        byte[] data = client.getData()
                .storingStatIn(stat)    // 保存節(jié)點(diǎn)狀態(tài)信息
                .forPath(nodePath);

        System.out.println(new String(data));
        System.out.println(stat.toString());
    }

    /**
     * 獲取所有子節(jié)點(diǎn)名稱
     *
     * @throws Exception
     */
    @Test
    public void getChildrenNode() throws Exception {
        List<String> nodes = client.getChildren().forPath("/curator");
        nodes.forEach((n) -> System.out.println(n));
    }

    /**
     * 判斷節(jié)點(diǎn)是否存在
     */
    @Test
    public void nodeExist() throws Exception {
        Stat stat = client.checkExists().forPath("/aaa");
        if (stat == null) {
            System.out.println("節(jié)點(diǎn)不存在");
        } else {
            System.out.println("節(jié)點(diǎn)存在" + stat);
        }
    }

7.3 設(shè)置Watch事件

  1. 設(shè)置一次失效watcher事件

        /**
         * 對(duì)節(jié)點(diǎn)設(shè)置watcher, 觸發(fā)一次后失效
         */
        @Test
        public void setWatcher() throws Exception {
            CountDownLatch latch = new CountDownLatch(2);
            client.getData().usingWatcher(new CuratorWatcher() {
                @Override
                public void process(WatchedEvent watchedEvent) throws Exception {
                    System.out.println(watchedEvent.getPath() + "觸發(fā)watcher事件: " + watchedEvent.getType());
                 // 只會(huì)執(zhí)行一次
                    latch.countDown();
                }
            }).forPath("/curator/imooc");
    
            // 等待操作cmd客戶端(set /workspace/curator/imooc 666), 觸發(fā)監(jiān)聽器, 回調(diào)process方法
            // 修改節(jié)點(diǎn)數(shù)據(jù), 觸發(fā)一次后失效, 所以程序永遠(yuǎn)不會(huì)結(jié)束
            latch.await();
        }
    
  2. 一次注冊(cè)N次監(jiān)聽wacher事件忽冻,不區(qū)分watch事件類型,不監(jiān)聽子節(jié)點(diǎn)NodeChildrenChanged事件

    /**
     * 利用nodeCache和Listener設(shè)置watch事件
     * 一次注冊(cè),N次監(jiān)聽
     * 缺點(diǎn)是多種類型Watch事件(NodeCreated, NodeDataChanged,NodeDeleted)都被稱為NodeChanged, 但是不監(jiān)聽NodeChildrenChanged事件
     * @throws Exception
     */
    @Test
    public void setWatcherByNodeCache() throws Exception {
        // 這次的監(jiān)聽器一直有效, 所以設(shè)置為5
        CountDownLatch latch = new CountDownLatch(5);

        NodeCache nodeCache = new NodeCache(client, "/curator/imooc");
        nodeCache.start(true);      // true啟動(dòng)時(shí)緩存當(dāng)前節(jié)點(diǎn), false啟動(dòng)時(shí)不緩存節(jié)點(diǎn)

        if (nodeCache.getCurrentData() == null) {
            System.out.println("節(jié)點(diǎn)初始化數(shù)據(jù)為空");
        } else {
            String data = new String(nodeCache.getCurrentData().getData());
            System.out.println("節(jié)點(diǎn)初始化數(shù)據(jù)為: " + data);
        }

        // 添加監(jiān)聽器, 等待節(jié)點(diǎn)被修改觸發(fā)監(jiān)聽器,執(zhí)行nodeChanged方法
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                // 節(jié)點(diǎn)刪除或不存在
                if(nodeCache.getCurrentData() == null) {
                    System.out.println("節(jié)點(diǎn)不存在");
                }
                // nodeCache.getCurrentData()是獲取節(jié)點(diǎn)對(duì)象ChildData, 
                // ChildData可以獲取節(jié)點(diǎn)路徑,數(shù)據(jù),狀態(tài)信息stat
                String data = new String(nodeCache.getCurrentData().getData());
                System.out.println("節(jié)點(diǎn)" + nodeCache.getCurrentData().getPath() + " 數(shù)據(jù)為:" + data );

                latch.countDown();
            }
        });

        // 使主程序不結(jié)束, 等待cmd客戶端修改節(jié)點(diǎn)觸發(fā)監(jiān)聽器(set /workspace/curator/imooc  777)
        // 一次注冊(cè),N次監(jiān)聽, 修改節(jié)點(diǎn)數(shù)據(jù)5次, 觸發(fā)10次watch事件后程序結(jié)束
        latch.await();
    }
  1. 設(shè)置區(qū)分事件類型Watch事件此疹,一次注冊(cè)僧诚,N次監(jiān)聽,區(qū)分事件類型蝗碎。因?yàn)?code>PathChildrenCache監(jiān)聽子節(jié)點(diǎn)湖笨,所以我們一般都設(shè)置為目標(biāo)節(jié)點(diǎn)的父節(jié)點(diǎn),然后在回調(diào)函數(shù)中篩選出目標(biāo)節(jié)點(diǎn)蹦骑。
    /**
     * 監(jiān)聽節(jié)點(diǎn), 需要異步初始化PathChildrenCache觸發(fā)監(jiān)聽
     * 
     * @throws Exception
     */
    @Test
    public void setWatchsByPathChildrenCache() throws Exception {
        // 設(shè)置需要監(jiān)聽的節(jié)點(diǎn)
        String nodePath = "/curator/imooc";

        // PathChildrenCache是監(jiān)聽所有子節(jié)點(diǎn), 所以設(shè)置為"/curator/imooc"的父節(jié)點(diǎn)/curator
        PathChildrenCache childCache = new PathChildrenCache(client, "/curator", true);
        /*
         * StartMode: 初始化方式
         * POST_INITIALIZED_EVENT:異步初始化慈省,初始化之后會(huì)觸發(fā)事件
         * NORMAL:異步初始化
         * BUILD_INITIAL_CACHE:同步初始化
         */
        childCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);

        // 注意這里獲取的是子節(jié)點(diǎn)的數(shù)據(jù),不是名稱
        List<ChildData> childDataList = childCache.getCurrentData();
        for (ChildData data : childDataList) {
            System.out.println(new String(data.getData()));
        }

        childCache.getListenable().addListener(new PathChildrenCacheListener() {
            /**
             * @param curatorFramework 就是client, 可以根據(jù)監(jiān)聽事件操作節(jié)點(diǎn), 比如監(jiān)聽到a節(jié)點(diǎn)修改了數(shù)據(jù), 那b節(jié)點(diǎn)就刪除client.delete().forPath("/b")
             * @param event  監(jiān)聽事件, 可以得到事件類型,節(jié)點(diǎn)名稱,節(jié)點(diǎn)數(shù)據(jù)等
             */
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
                String path = event.getData().getPath();

                // 由于PathChildrenCache監(jiān)聽/curator的所有子節(jié)點(diǎn),而我們只關(guān)心/curator/imooc, 所以使用衛(wèi)語句進(jìn)行排除
                if (!nodePath.equals(path)) {
                    return;
                }

                processEvent(event);
            }
        });
    }
    
    private void processEvent(PathChildrenCacheEvent event) {
        ChildData node = event.getData();
        switch (event.getType()) {
            case INITIALIZED:
                System.out.println("子節(jié)點(diǎn)初始化完成...");
                break;
            case CHILD_ADDED:   // 如果子節(jié)點(diǎn)已經(jīng)創(chuàng)建, 則在啟動(dòng)時(shí)會(huì)觸發(fā)該事件
                System.out.println("創(chuàng)建子節(jié)點(diǎn):" + node.getPath());
                System.out.println("子節(jié)點(diǎn)數(shù)據(jù):" + new String(node.getData()));
                break;
            case CHILD_UPDATED:
                System.out.println("修改子節(jié)點(diǎn):" + node.getPath());
                System.out.println("修改子節(jié)點(diǎn)數(shù)據(jù):" + new String(node.getData()));
                break;
            case CHILD_REMOVED:
                System.out.println("刪除子節(jié)點(diǎn):" + node.getPath());
                break;
            default:
                System.out.println("觸發(fā)Watch事件,類型為:" + event.getType());
        }
    }

7.2 <a name="watchconfig">統(tǒng)一配置文件管理</a>

統(tǒng)一配置文件管理的原理是利用watch事件,比如為了同步redis配置文件到redis集群

  1. 在zk上創(chuàng)建redisConfig節(jié)點(diǎn)
  2. 所有redis集群機(jī)器上都啟動(dòng)zk的 Java 客戶端眠菇,并對(duì)redisConfig節(jié)點(diǎn)設(shè)置watch事件
  3. 運(yùn)維人員使用命令行修改redisConfig節(jié)點(diǎn)的數(shù)據(jù)边败,set /workspace/conf/redis-config {"type":"update","url":"ftp://192.168.10.123/config/redis.xml"}
  4. 所有zk客戶端監(jiān)聽到DataChanged事件,查看節(jié)點(diǎn)數(shù)據(jù)捎废,解析數(shù)據(jù)后可知需要對(duì)redis配置文件的操作為update笑窜,配置文件地址為url
  5. 根據(jù)文件地址url下載redis配置文件登疗,替換原有的配置文件排截,重啟服務(wù)即可。

查看客戶端代碼

/**
 * 統(tǒng)一配置文件管理的原理是利用watch事件辐益,比如為了同步redis配置文件到redis集群
 *
 * 每臺(tái)機(jī)器上都執(zhí)行該類的main方法, 即啟動(dòng)zk客戶端.
 */
public class Client1 {

    public static CuratorFramework client = null;
    public static final String zkServerPath = "localhost:2181";

    static {
        RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
        client = CuratorFrameworkFactory.builder()
                .connectString(zkServerPath)
                .sessionTimeoutMs(10000).retryPolicy(retryPolicy)
                .namespace("workspace").build();
        client.start();
    }

    public final static String CONFIG_NODE = "/conf/redis-config";
    public final static String CONFIG_NODE_PATH = "/conf";
    public static CountDownLatch countDown = new CountDownLatch(10);

    public static void main(String[] args) throws Exception {
        System.out.println("client1 啟動(dòng)成功...");

        final PathChildrenCache childrenCache = new PathChildrenCache(client, CONFIG_NODE_PATH, true);
        childrenCache.start(StartMode.BUILD_INITIAL_CACHE);

        // 添加監(jiān)聽事件
        childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
                if (event.getData() == null) {
                    return;
                }

                String path = event.getData().getPath();
                // 只對(duì)/conf/redis-config節(jié)點(diǎn)的變化進(jìn)行處理
                if (!CONFIG_NODE.equals(path)) {
                    return;
                }
                processEvent(event);
            }
        });

        countDown.await();

        client.close();
    }

    public static void processEvent(PathChildrenCacheEvent event) throws InterruptedException {
        // 只監(jiān)聽/redic-config節(jié)點(diǎn)變化的事件, 不監(jiān)聽創(chuàng)建断傲、刪除節(jié)點(diǎn)事件
        if (!PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(event.getType())) {
            System.out.println(event.getType());
            return;
        }

        // 讀取節(jié)點(diǎn)數(shù)據(jù)
        String jsonConfig = new String(event.getData().getData());
        System.out.println("節(jié)點(diǎn)" + CONFIG_NODE_PATH + "的數(shù)據(jù)為: " + jsonConfig);
        if (jsonConfig.isEmpty()) {
            System.out.println("配置文件json為空, 請(qǐng)重新輸入");
        }
        JSONObject obj = JSON.parseObject(jsonConfig);//將json字符串轉(zhuǎn)換為json對(duì)象
        String type = obj.getString("type");
        String url = obj.getString("url");
        // 判斷操作類型,修改配置文件
        switch (type) {
            case "add":
                System.out.println("監(jiān)聽到新增的配置,文件路徑為<"+ url + ">, 準(zhǔn)備下載...");
                Thread.sleep(500);
                System.out.println("下載成功智政,已將配置文件添加到項(xiàng)目中");
                break;
            case "update":
                System.out.println("監(jiān)聽到新增的配置认罩,文件路徑為<"+ url + ">, 準(zhǔn)備下載...");
                Thread.sleep(500);
                System.out.println("下載成功,已將配置文件替換到項(xiàng)目中");
                break;
            case "delete":
                System.out.println("監(jiān)聽到需要?jiǎng)h除配置");
                Thread.sleep(100);
                System.out.println("成功刪除項(xiàng)目中原配置文件");
                break;
            default:
                System.out.println("無法識(shí)別操作類型:" + type);
        }
        // TODO 視情況統(tǒng)一重啟服務(wù)
    }
}

7.3 acl權(quán)限操作與認(rèn)證授權(quán)

  1. 創(chuàng)建節(jié)點(diǎn)時(shí)設(shè)置ACL權(quán)限
 /**
     * 創(chuàng)建節(jié)點(diǎn)時(shí)設(shè)置acl權(quán)限
     * 創(chuàng)建成功后使用命令行查看節(jié)點(diǎn)權(quán)限
     * getAcl /workspace/curator/imooc/myacl
     * 'digest,'imooc1:ee8R/pr2P4sGnQYNGyw2M5S5IMU=
     * : cdrwa
     * 'digest,'imooc2:Ux2+KXVIAs1OI24TQ/0A9Yh0/QU=
     * : rw
     *
     * @throws Exception
     */
    @Test
    public void createAcl() throws Exception {
        String nodePath = "/curator/imooc/myacl";
        List<ACL> acls = new ArrayList<>();
        Id imooc1 = new Id("digest", getDigestUserPwd("imooc1:123456"));
        Id imooc2 = new Id("digest", getDigestUserPwd("imooc2:666666"));

        // 用戶imooc1擁有所有權(quán)限, imooc2擁有讀寫權(quán)限
        acls.add(new ACL(ZooDefs.Perms.ALL, imooc1));
        acls.add(new ACL(ZooDefs.Perms.READ | ZooDefs.Perms.WRITE, imooc2));

        // 創(chuàng)建節(jié)點(diǎn)
        client.create()
                .creatingParentsIfNeeded()
                // 設(shè)置節(jié)點(diǎn)的acl權(quán)限, false表示不對(duì)父節(jié)點(diǎn)/curator/imooc/生效, 當(dāng)父節(jié)點(diǎn)已存在時(shí),true也不生效
                .withACL(acls, false)
                .forPath(nodePath, "123".getBytes());

    }

    public static String getDigestUserPwd(String id) {
        String digest = "";
        try {
            digest = DigestAuthenticationProvider.generateDigest(id);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return digest;
    }

執(zhí)行完上面的代碼续捂,創(chuàng)建節(jié)點(diǎn)成功后垦垂,使用命令行查看acl權(quán)限。

# imooc1擁有全部權(quán)限, imooc2擁有讀寫權(quán)限疾忍,與代碼設(shè)置一直
[zk: localhost:2181(CONNECTED) 19] getAcl /workspace/curator/imooc/myacl
'digest,'imooc1:ee8R/pr2P4sGnQYNGyw2M5S5IMU=
: cdrwa
'digest,'imooc2:Ux2+KXVIAs1OI24TQ/0A9Yh0/QU=
: rw
# 不登錄操作節(jié)點(diǎn), 提示權(quán)限不合法
[zk: localhost:2181(CONNECTED) 20] set /workspace/curator/imooc/myacl 000
Authentication is not valid : /workspace/curator/imooc/myacl
  1. 修改具有ACL權(quán)限控制節(jié)點(diǎn)的數(shù)據(jù)
    1. 使用用戶登錄并創(chuàng)建客戶端
    2. 修改節(jié)點(diǎn)數(shù)據(jù)
    3. 重新設(shè)置節(jié)點(diǎn)ACL權(quán)限(需要用戶具有admin權(quán)限)
    /**
     * 獲取權(quán)限限制的節(jié)點(diǎn)數(shù)據(jù), 重新設(shè)置節(jié)點(diǎn)ACL
     * @throws Exception
     */
    @Test
    public void getDataAndSetAcl() throws Exception {
        String nodePath = "/curator/imooc/myacl";
        // 上個(gè)方法中設(shè)置了myacl的權(quán)限, client沒有登錄, 所以無法修改數(shù)據(jù), 會(huì)拋出異常KeeperException$NoAuthException
        //client.setData().forPath(nodePath, "aaa".getBytes());

        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

        // 使用用戶imooc1登錄客戶端, imooc1具有管理權(quán)限
        CuratorFramework authClient = CuratorFrameworkFactory.builder()
                .connectString(zkServerPath)
                .authorization("digest", "imooc1:123456".getBytes())
                .sessionTimeoutMs(10000).retryPolicy(retryPolicy)
                .namespace("workspace").build();
        authClient.start();

        authClient.setData().forPath(nodePath, "aaa".getBytes());

        /*
         * 修改數(shù)據(jù)后使用以下兩個(gè)命令在Cli查看數(shù)據(jù)是否修改成功
         * addauth digest imooc2:666666
         * get /workspace/curator/imooc/myacl
         */

        // 設(shè)置節(jié)點(diǎn)的ACL權(quán)限,imooc2有了刪除權(quán)限. 注意這里是重新設(shè)置, 而不是添加權(quán)限
        List<ACL> acls = new ArrayList<>();
        Id imooc1 = new Id("digest", getDigestUserPwd("imooc1:123456"));
        Id imooc2 = new Id("digest", getDigestUserPwd("imooc2:666666"));
        acls.add(new ACL(ZooDefs.Perms.ALL, imooc1));
        acls.add(new ACL(ZooDefs.Perms.READ | ZooDefs.Perms.WRITE | ZooDefs.Perms.DELETE, imooc2));

        authClient.setACL().withACL(acls).forPath(nodePath);

        // Cli使用 getAcl /workspace/curator/imooc/myacl  查看節(jié)點(diǎn)權(quán)限
    }

8 zookeeper 實(shí)現(xiàn)原理

8.1 為dubbo提供動(dòng)態(tài)的服務(wù)注冊(cè)和發(fā)現(xiàn)

dubbo無法動(dòng)態(tài)注冊(cè)和發(fā)現(xiàn)

比如項(xiàng)目中有多個(gè)訂單服務(wù),每個(gè)服務(wù)都是一臺(tái)機(jī)器床三,每個(gè)客戶端(這是Order請(qǐng)求的客戶端一罩,不是zk客戶端)都有一份服務(wù)提供者列表。

高并發(fā)時(shí)需要添加多臺(tái)機(jī)器或服務(wù)down掉了撇簿,服務(wù)的提供者發(fā)生了變化聂渊,結(jié)果客戶端并不知道差购。

要想得到最新的服務(wù)提供者的URL列表,必須得手工更新配置文件才行汉嗽,確實(shí)很不方便欲逃。

20200303200126.png

這就是客戶端和服務(wù)提供者的緊耦合,想解除這個(gè)耦合饼暑,非得增加一個(gè)中間層不可稳析。

zookeeper注冊(cè)中心

所以應(yīng)該有個(gè)注冊(cè)中心,首先給這些服務(wù)命名(例如orderService)弓叛,其次那些新增OrderService 都可以在這里注冊(cè)一下彰居,客戶端就到這里來查詢,只需要給出名稱orderService撰筷,注冊(cè)中心就可以給出一個(gè)可以使用的url陈惰, 再也不怕服務(wù)提供者的動(dòng)態(tài)增減了。

e1c0bc56c20024152140868979a5f98.png

zookeeper就可以充當(dāng)上文中的注冊(cè)中心毕籽,創(chuàng)建節(jié)點(diǎn)/orderService抬闯,提供訂單服務(wù)的機(jī)器需要啟動(dòng)一個(gè)zk客戶端,注冊(cè)一個(gè)node1節(jié)點(diǎn)关筒,節(jié)點(diǎn)數(shù)據(jù)保存服務(wù)的url

20200303200412.png

/orderService 表達(dá)了一個(gè)服務(wù)的概念溶握, 下面的每個(gè)節(jié)點(diǎn)表示了一個(gè)服務(wù)的實(shí)例。 例如/orderService/node2表示的orderService 的第二個(gè)實(shí)例平委, 每個(gè)節(jié)點(diǎn)上可以記錄下該實(shí)例的url , 這樣就可以查詢了奈虾。

當(dāng)然這個(gè)注冊(cè)中心必須得能和各個(gè)服務(wù)實(shí)例通信,如果某個(gè)服務(wù)實(shí)例不幸down掉了廉赔,那它在樹結(jié)構(gòu)中對(duì)于的節(jié)點(diǎn)也必須刪除肉微,這樣客戶端就查詢不到了。

20200303200444.png

注冊(cè)中心zookeeper就是和各個(gè)服務(wù)實(shí)例node之間建立Session蜡塌,讓各個(gè)服務(wù)實(shí)例的zk客戶端定時(shí)發(fā)送心跳碉纳,如果過了特定時(shí)間收不到心跳,就認(rèn)為這個(gè)服務(wù)實(shí)例node掛掉了馏艾,Session 過期劳曹, 把它從樹形結(jié)構(gòu)中刪除。

8.2 用于實(shí)現(xiàn)分布式鎖

同一個(gè)進(jìn)程中琅摩,多個(gè)線程訪問共享資源铁孵,可以使用Java提供的synchronized等鎖就可以實(shí)現(xiàn)安全訪問,但是在分布式系統(tǒng)中房资,程序都跑在不同機(jī)器的不同進(jìn)程中蜕劝,多個(gè)系統(tǒng)(進(jìn)程)訪問共享資源,就需要一個(gè)分布式鎖

和synchronized一樣,保證一個(gè)資源只能同時(shí)被一個(gè)節(jié)點(diǎn)搶到即可岖沛。誰能搶先在zookeeper創(chuàng)建一個(gè)/distribute_lock的節(jié)點(diǎn)就表示搶到這個(gè)鎖了暑始,然后讀寫資源,讀寫完以后就把/distribute_lock節(jié)點(diǎn)刪除婴削,其他進(jìn)程再來搶廊镜。

這樣存在一個(gè)缺點(diǎn),某個(gè)系統(tǒng)可能會(huì)多次搶到唉俗,不太公平嗤朴。

可以讓這些系統(tǒng)在注冊(cè)中心zookeeper的/distribute_lock下都創(chuàng)建順序節(jié)點(diǎn),會(huì)自動(dòng)給每個(gè)節(jié)點(diǎn)一個(gè)編號(hào)互躬,會(huì)是這個(gè)樣子:

20200303200531.png

然后各個(gè)系統(tǒng)去檢查自己的編號(hào)播赁,誰的編號(hào)小就認(rèn)為誰持有了鎖, 例如系統(tǒng)1吼渡。

系統(tǒng)1持有了鎖容为,就可以對(duì)共享資源進(jìn)行操作了, 操作完成以后process_01這個(gè)節(jié)點(diǎn)刪除寺酪, 再創(chuàng)建一個(gè)新的節(jié)點(diǎn)(編號(hào)變成process_04了):


image.png

其他系統(tǒng)一看坎背,編號(hào)為01的刪除了,再看看誰是最小的吧寄雀,是process_02得滤,那就認(rèn)為系統(tǒng)2持有了鎖,可以對(duì)共享資源操作了盒犹。 操作完成以后也要把process_02節(jié)點(diǎn)刪除懂更,創(chuàng)建新的節(jié)點(diǎn)。這時(shí)候process_03就是最小的了急膀,可以持有鎖了沮协。

20200303200640.png

8.3 zookeeper 高可用

服務(wù)注冊(cè)于發(fā)現(xiàn)和分布式鎖的例子,都加入了一個(gè)中間層zookeeper卓嫂,但是引入了一個(gè)重要的問題:

? 如果zookeeper掛掉慷暂,所有服務(wù)都依賴于zookeeper,那么就無法注冊(cè)服務(wù)和發(fā)現(xiàn)服務(wù)晨雳,也無法獲取分布式鎖了行瑞,所以必須保證注冊(cè)中心zookeeper的高可用。

為了實(shí)現(xiàn)高可用餐禁,zookeeper維護(hù)了一個(gè)集群血久,一主多從結(jié)構(gòu),如下圖所示:

20200303200709.png

zookeeper會(huì)從Server集群選舉出一個(gè)Leader節(jié)點(diǎn)(這里的節(jié)點(diǎn)是指服務(wù)器帮非,不是 Znode)氧吐,用于接收寫/讀請(qǐng)求绷旗。更新數(shù)據(jù)時(shí),首先更新到Leader副砍,再同步到follwer

Server集群其他均為follwer庄岖,用于接收讀請(qǐng)求豁翎,直接從當(dāng)前follower Server讀取。但是又出現(xiàn)了主從數(shù)據(jù)一致性問題隅忿。

如何保證zookeeper主從節(jié)點(diǎn)(Leader和follower)的數(shù)據(jù)一致性呢心剥?

為了保證主從節(jié)點(diǎn)的數(shù)據(jù)一致性,ZooKeeper 采用了 ZAB 協(xié)議(Zookeeper Atomic Broadcast)背桐,這種協(xié)議非常類似于一致性算法 Paxos 和 Raft优烧。 ZAB 協(xié)議所定義的三種節(jié)點(diǎn)狀態(tài):

Looking:選舉狀態(tài)。

Leading:Leader 節(jié)點(diǎn)(主節(jié)點(diǎn))所處狀態(tài)链峭。

Following:Follower 節(jié)點(diǎn)(從節(jié)點(diǎn))所處的狀態(tài)畦娄。

zk客戶端會(huì)隨機(jī)的鏈接到 zookeeper 集群中的一個(gè)Leaderfollower節(jié)點(diǎn),如果是讀請(qǐng)求弊仪,就直接從當(dāng)前節(jié)點(diǎn)中讀取數(shù)據(jù)熙卡;如果是寫請(qǐng)求,那么節(jié)點(diǎn)就會(huì)向 Leader提交事務(wù)励饵,Leader接收到事務(wù)提交驳癌,會(huì)廣播該事務(wù),只要超過半數(shù)節(jié)點(diǎn)寫入成功役听,該事務(wù)就會(huì)被提交颓鲜,每一個(gè)事務(wù)都會(huì)使用zxid持久化到日志中,用于zk崩潰時(shí)恢復(fù)節(jié)點(diǎn)典予。

另外甜滨,Zookeeper是一個(gè)樹形結(jié)構(gòu),具有順序性很多操作都要先檢查才能確定是否可以執(zhí)行熙参,比如P1的事務(wù)t1可能是創(chuàng)建節(jié)點(diǎn)"/a"艳吠,t2可能是創(chuàng)建節(jié)點(diǎn)"/a/b",只有先創(chuàng)建了父節(jié)點(diǎn)"/a"孽椰,才能創(chuàng)建子節(jié)點(diǎn)"/a/b"昭娩。

為了實(shí)現(xiàn)這一點(diǎn),Zab協(xié)議要保證同一個(gè)Leader發(fā)起的事務(wù)要按順序被執(zhí)行黍匾,同時(shí)還要保證只有先前Leader的事務(wù)被執(zhí)行之后栏渺,新選舉出來的Leader才能再次發(fā)起事務(wù)。

8.4 Zookeeper 的崩潰恢復(fù)

如果主節(jié)點(diǎn)Leader宕機(jī)锐涯,那么如何恢復(fù)服務(wù)呢磕诊?

1. 領(lǐng)導(dǎo)選舉Leader election

選舉階段,此時(shí)集群中的節(jié)點(diǎn)處于 Looking 狀態(tài)。它們會(huì)各自向其他節(jié)點(diǎn)發(fā)起投票霎终,投票當(dāng)中包含自己的服務(wù)器 ID 和最新事務(wù) ID(ZXID)滞磺。

image.png

接下來,節(jié)點(diǎn)會(huì)用自身的 ZXID 和從其他節(jié)點(diǎn)接收到的 ZXID 做比較莱褒,如果發(fā)現(xiàn)別人家的 ZXID 比自己大击困,也就是數(shù)據(jù)比自己新,那么就重新發(fā)起投票广凸,投票給目前已知最大的 ZXID 所屬節(jié)點(diǎn)阅茶。

de93fb73e29d77d46f198ee82c51577.png

每次投票后,服務(wù)器都會(huì)統(tǒng)計(jì)投票數(shù)量谅海,判斷是否有某個(gè)節(jié)點(diǎn)得到半數(shù)以上的投票脸哀。如果存在這樣的節(jié)點(diǎn),該節(jié)點(diǎn)將會(huì)成為準(zhǔn) Leader扭吁,狀態(tài)變?yōu)?Leading撞蜂。其他節(jié)點(diǎn)的狀態(tài)變?yōu)?Following。

20200303200809.png

這就相當(dāng)于侥袜,一群武林高手經(jīng)過激烈的競爭谅摄,選出了武林盟主。

2. 發(fā)現(xiàn) Discovery

發(fā)現(xiàn)階段系馆,用于在從節(jié)點(diǎn)中發(fā)現(xiàn)最新的 ZXID 和事務(wù)日志送漠。或許有人會(huì)問:既然 Leader 被選為主節(jié)點(diǎn)由蘑,已經(jīng)是集群里數(shù)據(jù)最新的了闽寡,為什么還要從節(jié)點(diǎn)中尋找最新事務(wù)呢?

這是為了防止某些意外情況尼酿,比如因網(wǎng)絡(luò)原因在上一階段產(chǎn)生多個(gè) Leader 的情況爷狈。

所以這一階段,Leader 集思廣益裳擎,接收所有 Follower 發(fā)來各自的最新 epoch 值涎永。Leader 從中選出最大的 epoch,基于此值加 1鹿响,生成新的 epoch 分發(fā)給各個(gè) Follower羡微。

各個(gè) Follower 收到全新的 epoch 后,返回 ACK 給 Leader惶我,帶上各自最大的 ZXID 和歷史事務(wù)日志妈倔。Leader 選出最大的 ZXID,并更新自身歷史日志绸贡。

3. 同步 Synchronization

同步階段盯蝴,把 Leader 剛才收集得到的最新歷史事務(wù)日志毅哗,同步給集群中所有的 Follower。只有當(dāng)半數(shù) Follower 同步成功捧挺,這個(gè)準(zhǔn) Leader 才能成為正式的 Leader虑绵。

自此,故障恢復(fù)正式完成闽烙。

8.5 zookeeper 數(shù)據(jù)寫入過程

寫入數(shù)據(jù)就涉及到了 ZAB協(xié)議的 BroadCast (廣播)階段蒸殿,簡單來說,就是 Zookeeper 常規(guī)情況下更新數(shù)據(jù)的時(shí)候鸣峭,由 Leader 廣播到所有的 Follower。詳細(xì)過程如下:

  1. zk客戶端發(fā)出寫入數(shù)據(jù)請(qǐng)求給任意Follower酥艳。

  2. Follower 把寫入數(shù)據(jù)請(qǐng)求轉(zhuǎn)發(fā)給 Leader摊溶。

  3. Leader 采用二階段提交方式,先發(fā)送廣播給 Follower充石。

  4. Follower 接到 Propose 消息莫换,寫入日志成功后,返回 ACK 消息給 Leader骤铃。

  5. Leader 接到半數(shù)以上 ACK 消息拉岁,返回成功給客戶端,并且廣播 Commit 請(qǐng)求給 Follower惰爬。

20200303200856.png

9 zookeeper 分布式鎖

9.1 Curator與Spring的結(jié)合

見參考文檔2

9.2 什么是分布式鎖

9.2 實(shí)現(xiàn)分布式鎖

分布式一致性算法

集群中有兩個(gè)數(shù)據(jù)庫A和B喊暖,為了保證一致性,所以A和B需要同步數(shù)據(jù)撕瞧。當(dāng)User更新了數(shù)據(jù)庫A的數(shù)據(jù)value后陵叽,User從數(shù)據(jù)庫B讀取數(shù)據(jù)value,此時(shí)會(huì)出現(xiàn)三種情況:

  1. 強(qiáng)一致性丛版,value==2巩掺。強(qiáng)一致性需要讓同步過程非常快(很難實(shí)現(xiàn))页畦;或者利用分布式鎖胖替,在讀取數(shù)據(jù)庫B前阻塞住,等待同步完成后釋放鎖
  2. 弱一致性豫缨,value==1 独令。數(shù)據(jù)更新后,如果能容忍后續(xù)的訪問只能訪問到部分或者全部訪問不到好芭,則是弱一致性记焊。最終一致性就屬于弱一致性。
  3. 最終一致性栓撞,最終value==2遍膜。一段時(shí)間后碗硬,節(jié)點(diǎn)間的數(shù)據(jù)會(huì)最終達(dá)到一致狀態(tài),但不保證在任意時(shí)刻任意節(jié)點(diǎn)上的同一份數(shù)據(jù)都是相同的
一致性.png

更多一致性問題參考文章強(qiáng)一致性瓢颅、順序一致性恩尾、弱一致性和共識(shí)

待補(bǔ)充

后面根據(jù)極客時(shí)間《zookeeper實(shí)戰(zhàn)與源碼解析》(8小時(shí)視頻)補(bǔ)充筆記挽懦,包括

  1. 實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)翰意,
  2. 解析paxos和raft,對(duì)比Chubby信柿,使用etcd冀偶,
  3. 存儲(chǔ)結(jié)構(gòu),存儲(chǔ)源碼渔嚷,
  4. 客戶端服務(wù)端通信源碼进鸠,
  5. 節(jié)點(diǎn)選舉,ZAB

根據(jù)博客等逐步更新一下內(nèi)容:

  1. CAP理論
  2. 服務(wù)端同步原理形病,
  3. 客戶端響應(yīng)原理客年,
  4. 可視化客戶端工具ZooInspector和exhibitor
  5. zookeeper異步初始化的源碼分析,eventThread漠吻,sendThread

總結(jié):
啥都不懂公眾號(hào), 觀其大略有印象;
快速入門看周陽, 短小生動(dòng)門檻低;
想要開發(fā)看慕課, 制作精良能實(shí)戰(zhàn);
深入理解看極客, 大牛源碼說原理.
依次耗時(shí)更長量瓜,學(xué)習(xí)曲線更陡峭,但是也更深入

推薦閱讀

  1. 什么是zookeeper - 碼農(nóng)翻身途乃,講了zookeeper誕生是為了解決哪些問題绍傲,即zk的作用
  2. 分布式一致性算法 - 碼農(nóng)翻身
  3. 強(qiáng)一致性、順序一致性耍共、弱一致性和共識(shí)
  4. 什么是zookeeper - 程序員小灰
  5. 如何用zookeeper實(shí)現(xiàn)分布式鎖 - 程序員小灰
  6. zookeeper 面試題 - 附答案唧取,用于檢查學(xué)習(xí)成果和復(fù)習(xí)
  7. 觀察者模式,zookeeper是一個(gè)基于觀察者模式設(shè)計(jì)的分布式服務(wù)管理框架

參考文檔

  1. ZooKeeper分布式專題與Dubbo微服務(wù)入門 - 慕課網(wǎng)
  2. zookeeper 代碼倉庫 - github
  3. 深入淺出理解Zookeeper - 周陽
  4. Zookeeper實(shí)戰(zhàn)與源碼剖析 - 極客時(shí)間
  5. zookeeper源碼解讀
  6. 什么是zookeeper - 碼農(nóng)翻身
  7. 分布式一致性算法 - 碼農(nóng)翻身
  8. 強(qiáng)一致性划提、順序一致性枫弟、弱一致性和共識(shí)
  9. 什么是zookeeper - 程序員小灰
  10. 如何用zookeeper實(shí)現(xiàn)分布式鎖 - 程序員小灰
  11. Java中的樂觀鎖
  12. zookeeper 面試題 - 附答案
  13. Java 異步實(shí)現(xiàn)的幾種方式

錯(cuò)誤總結(jié)

  1. ZooKeeper 啟動(dòng)報(bào)錯(cuò)java.lang.NumberFormatException

  2. Curator NodeCache的錯(cuò)誤使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鹏往,隨后出現(xiàn)的幾起案子淡诗,更是在濱河造成了極大的恐慌,老刑警劉巖伊履,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韩容,死亡現(xiàn)場離奇詭異,居然都是意外死亡唐瀑,警方通過查閱死者的電腦和手機(jī)群凶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哄辣,“玉大人请梢,你說我怎么就攤上這事赠尾。” “怎么了毅弧?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵气嫁,是天一觀的道長。 經(jīng)常有香客問我够坐,道長寸宵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任元咙,我火速辦了婚禮梯影,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庶香。我一直安慰自己甲棍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布脉课。 她就那樣靜靜地躺著,像睡著了一般财异。 火紅的嫁衣襯著肌膚如雪倘零。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天戳寸,我揣著相機(jī)與錄音呈驶,去河邊找鬼。 笑死疫鹊,一個(gè)胖子當(dāng)著我的面吹牛袖瞻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拆吆,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼聋迎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枣耀?” 一聲冷哼從身側(cè)響起霉晕,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捞奕,沒想到半個(gè)月后牺堰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡颅围,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年伟葫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片院促。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡筏养,死狀恐怖斧抱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撼玄,我是刑警寧澤夺姑,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站掌猛,受9級(jí)特大地震影響盏浙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荔茬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一废膘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧慕蔚,春花似錦丐黄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坏瞄,卻和暖如春桂对,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸠匀。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工蕉斜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缀棍。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓宅此,卻偏偏與公主長得像,于是被迫代替她去往敵國和親爬范。 傳聞我的和親對(duì)象是個(gè)殘疾皇子父腕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348