Zookeeper

什麼是Zookeeper

Zookeeper是一個分佈式開源框架,提供了協調分佈式應用的基本服務,它向外部應用暴露一組通用服務——分佈式同步(Distributed Synchronization)、命名服務(Naming Service)、集羣維護(Group Maintenance)等,簡化分佈式應用協調及其管理的難度,提供高性能的分佈式服務。ZooKeeper本身可以以單機模式安裝運行,不過它的長處在於通過分佈式ZooKeeper集羣(一個Leader,多個Follower),基於一定的策略來保證ZooKeeper集羣的穩定性和可用性,從而實現分佈式應用的可靠性。
1、Zookeeper是爲別的分佈式程序服務的
2、Zookeeper本身就是一個分佈式程序(只要有半數以上節點存活,zk就能正常服務)
3、Zookeeper所提供的服務涵蓋:主從協調、服務器節點動態上下線、統一配置管理、分佈式共享鎖、統> 一名稱服務等
4、雖然說可以提供各種服務,但是zookeeper在底層其實只提供了兩個功能:
管理(存儲,讀取)用戶程序提交的數據(類似namenode中存放的metadata);
併爲用戶程序提供數據節點監聽服務;

Zookeeper集羣機制

Zookeeper集羣的角色: Leader 和 follower
只要集羣中有半數以上節點存活,集羣就能提供服務

Zookeeper特性

1、Zookeeper:一個leader,多個follower組成的集羣
2、全局數據一致:每個server保存一份相同的數據副本,client無論連接到哪個server,數據都是一致的
3、分佈式讀寫,更新請求轉發,由leader實施
4、更新請求順序進行,來自同一個client的更新請求按其發送順序依次執行
5、數據更新原子性,一次數據更新要麼成功,要麼失敗
6、實時性,在一定時間範圍內,client能讀到最新數據

Zookeeper數據結構

1、層次化的目錄結構,命名符合常規文件系統規範(類似文件系統)
在這裏插入圖片描述

2、每個節點在zookeeper中叫做znode,並且其有一個唯一的路徑標識

3、節點Znode可以包含數據和子節點(但是EPHEMERAL類型的節點不能有子節點)
節點類型
a、Znode有兩種類型:
短暫(ephemeral)(create -e /app1/test1 “test1” 客戶端斷開連接zk刪除ephemeral類型節點)
持久(persistent) (create -s /app1/test2 “test2” 客戶端斷開連接zk不刪除persistent類型節點)
b、Znode有四種形式的目錄節點(默認是persistent )
PERSISTENT
PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )
EPHEMERAL
EPHEMERAL_SEQUENTIAL
c、創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護
在這裏插入圖片描述
d、在分佈式系統中,順序號可以被用於爲所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序

Zookeeper應用場景

數據發佈與訂閱(配置中心)
發佈與訂閱模型,即所謂的配置中心,顧名思義就是發佈者將數據發佈到ZK節點上,供訂閱者動態獲取數據,實現配置信息的集中式管理和動態更新。例如全局的配置信息,服務式服務框架的服務地址列表等就非常適合使用。

負載均衡

這裏說的負載均衡是指軟負載均衡。在分佈式環境中,爲了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。而消費者就須要在這些對等的服務器中選擇一個來執行相關的業務邏輯,其中比較典型的是消息中間件中的生產者,消費者負載均衡。
消息中間件中發佈者和訂閱者的負載均衡,linkedin開源的KafkaMQ和阿里開源的 metaq都是通過zookeeper來做到生產者、消費者的負載均衡。這裏以metaq爲例如講下:
生產者負載均衡:metaq發送消息的時候,生產者在發送消息的時候必須選擇一臺broker上的一個分區來發送消息,因此metaq在運行過程中,會把所有broker和對應的分區信息全部註冊到ZK指定節點上,默認的策略是一個依次輪詢的過程,生產者在通過ZK獲取分區列表之後,會按照brokerId和partition的順序排列組織成一個有序的分區列表,發送的時候按照從頭到尾循環往復的方式選擇一個分區來發送消息。
消費負載均衡: 在消費過程中,一個消費者會消費一個或多個分區中的消息,但是一個分區只會由一個消費者來消費。MetaQ的消費策略是:

  1. 每個分區針對同一個group只掛載一個消費者。
  2. 如果同一個group的消費者數目大於分區數目,則多出來的消費者將不參與消費。
  3. 如果同一個group的消費者數目小於分區數目,則有部分消費者需要額外承擔消費任務。
    在某個消費者故障或者重啓等情況下,其他消費者會感知到這一變化(通過 zookeeper watch消費者列表),然後重新進行負載均衡,保證所有的分區都有消費者進行消費。
    命名服務(Naming Service)

命名服務也是分佈式系統中比較常見的一類場景。在分佈式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等信息。被命名的實體通常可以是集羣中的機器,提供的服務地址,遠程對象等等——這些我們都可以統稱他們爲名字(Name)。其中較爲常見的就是一些分佈式服務框架中的服務地址列表。通過調用ZK提供的創建節點的API,能夠很容易創建一個全局唯一的path,這個path就可以作爲一個名稱。
阿里巴巴集團開源的分佈式服務框架Dubbo中使用ZooKeeper來作爲其命名服務,維護全局的服務地址列表, 點擊這裏查看Dubbo開源項目。在Dubbo實現中:
服務提供者在啓動的時候,向ZK上的指定節點/dubbo/serviceName/providersURL/dubbo/{serviceName}/providers目錄下寫入自己的URL地址,這個操作就完成了服務的發佈。 服務消費者啓動的時候,訂閱/dubbo/{serviceName}/providers目錄下的提供者URL地址, 並向/dubbo/serviceName/consumersURLZKDubbo/dubbo/{serviceName} /consumers目錄下寫入自己的URL地址。 注意,所有向ZK上註冊的地址都是臨時節點,這樣就能夠保證服務提供者和消費者能夠自動感應資源的變化。 另外,Dubbo還有針對服務粒度的監控,方法是訂閱/dubbo/{serviceName}目錄下所有提供者和消費者的信息。

分佈式通知/協調

ZooKeeper中特有watcher註冊與異步通知機制,能夠很好的實現分佈式環境下不同系統之間的通知與協調,實現對數據變更的實時處理。使用方法通常是不同系統都對ZK上同一個znode進行註冊,監聽znode的變化(包括znode本身內容及子節點的),其中一個系統update了znode,那麼另一個系統能夠收到通知,並作出相應處理

  1. 另一種心跳檢測機制:檢測系統和被檢測系統之間並不直接關聯起來,而是通過zk上某個節點關聯,大大減少系統耦合。
  2. 另一種系統調度模式:某系統有控制檯和推送系統兩部分組成,控制檯的職責是控制推送系統進行相應的推送工作。管理人員在控制檯作的一些操作,實際上是修改了ZK上某些節點的狀態,而ZK就把這些變化通知給他們註冊Watcher的客戶端,即推送系統,於是,作出相應的推送任務。
  3. 另一種工作彙報模式:一些類似於任務分發系統,子任務啓動後,到zk來註冊一個臨時節點,並且定時將自己的進度進行彙報(將進度寫回這個臨時節點),這樣任務管理者就能夠實時知道任務進度。
    總之,使用zookeeper來進行分佈式通知和協調能夠大大降低系統之間的耦合
    集羣管理與Master選舉
  4. 集羣機器監控:這通常用於那種對集羣中機器狀態,機器在線率有較高要求的場景,能夠快速對集羣中機器變化作出響應。這樣的場景中,往往有一個監控系統,實時檢測集羣機器是否存活。過去的做法通常是:監控系統通過某種手段(比如ping)定時檢測每個機器,或者每個機器自己定時向監控系統彙報“我還活着”。 這種做法可行,但是存在兩個比較明顯的問題:
  5. 集羣中機器有變動的時候,牽連修改的東西比較多。
  6. 有一定的延時。
    利用ZooKeeper有兩個特性,就可以實現另一種集羣機器存活性監控系統:
  7. 客戶端在節點 x 上註冊一個Watcher,那麼如果 x?的子節點變化了,會通知該客戶端。
  8. 創建EPHEMERAL類型的節點,一旦客戶端和服務器的會話結束或過期,那麼該節點就會消失。
    例如,監控系統在 /clusterServers 節點上註冊一個Watcher,以後每動態加機器,那麼就往 /clusterServers 下創建一個 EPHEMERAL類型的節點:/clusterServers/{hostname}. 這樣,監控系統就能夠實時知道機器的增減情況,至於後續處理就是監控系統的業務了。
  9. Master選舉則是zookeeper中最爲經典的應用場景了。
    在分佈式環境中,相同的業務應用分佈在不同的機器上,有些業務邏輯(例如一些耗時的計算,網絡I/O處理),往往只需要讓整個集羣中的某一臺機器進行執行,其餘機器可以共享這個結果,這樣可以大大減少重複勞動,提高性能,於是這個master選舉便是這種場景下的碰到的主要問題。
    利用ZooKeeper的強一致性,能夠保證在分佈式高併發情況下節點創建的全局唯一性,即:同時有多個客戶端請求創建 /currentMaster 節點,最終一定只有一個客戶端請求能夠創建成功。利用這個特性,就能很輕易的在分佈式環境中進行集羣選取了。
    另外,這種場景演化一下,就是動態Master選舉。這就要用到EPHEMERAL_SEQUENTIAL類型節點的特性了。
    上文中提到,所有客戶端創建請求,最終只有一個能夠創建成功。在這裏稍微變化下,就是允許所有請求都能夠創建成功,但是得有個創建順序,於是所有的請求最終在ZK上創建結果的一種可能情況是這樣: /currentMaster/{sessionId}-1 ,/currentMaster/{sessionId}-2,/currentMaster/{sessionId}-3 …… 每次選取序列號最小的那個機器作爲Master,如果這個機器掛了,由於他創建的節點會馬上小時,那麼之後最小的那個機器就是Master了。
  10. 在搜索系統中,如果集羣中每個機器都生成一份全量索引,不僅耗時,而且不能保證彼此之間索引數據一致。因此讓集羣中的Master來進行全量索引的生成,然後同步到集羣中其它機器。另外,Master選舉的容災措施是,可以隨時進行手動指定master,就是說應用在zk在無法獲取master信息時,可以通過比如http方式,向一個地方獲取master。
  11. 在Hbase中,也是使用ZooKeeper來實現動態HMaster的選舉。在Hbase實現中,會在ZK上存儲一些ROOT表的地址和HMaster的地址,HRegionServer也會把自己以臨時節點(Ephemeral)的方式註冊到Zookeeper中,使得HMaster可以隨時感知到各個HRegionServer的存活狀態,同時,一旦HMaster出現問題,會重新選舉出一個HMaster來運行,從而避免了HMaster的單點問題

分佈式鎖

分佈式鎖,這個主要得益於 ZooKeeper 爲我們保證了數據的強一致性。鎖服務可以分爲兩類,一個是 保持獨佔,另一個是 控制時序。

  1. 所謂保持獨佔,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把 zk 上的一個 znode 看作是一把鎖,通過 create znode 的方式來實現。所有客戶端都去創建 /distribute_lock 節點,最終成功創建的那個客戶端也即擁有了這把鎖。
  2. 控制時序,就是所有視圖來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全局時序了。做法和上面基本類似,只是這裏 /distributelock 已經預先存在,客戶端在它下面創建臨時有序節點(這個可以通過節點的屬性控制:CreateMode.EPHEMERALSEQUENTIAL 來指定)。Zk 的父節點(/distribute_lock)維持一份 sequence, 保證子節點創建的時序性,從而也形成了每個客戶端的全局時序。

Zookeeper安裝

Zookeeper windows環境安裝
環境要求:必須要有jdk環境,本次講課使用jdk1.8

1.安裝jdk
2.安裝Zookeeper. 在官網http://zookeeper.apache.org/下載zookeeper.我下載的是zookeeper-3.4.6版本。
解壓zookeeper-3.4.6至D:\machine\zookeeper-3.4.6.
在D:\machine 新建data及log目錄。
3.ZooKeeper的安裝模式分爲三種,分別爲:單機模式(stand-alone)、集羣模式和集羣僞分佈模式。ZooKeeper 單機模式的安裝相對比較簡單,如果第一次接觸ZooKeeper的話,建議安裝ZooKeeper單機模式或者集羣僞分佈模式。
安裝單擊模式。 至D:\machine\zookeeper-3.4.6\conf 複製 zoo_sample.cfg 並粘貼到當前目錄下,命名zoo.cfg.

Zookeeper集羣環境搭建(linux)
環境要求:必須要有jdk環境,本次講課使用jdk1.8
結構
一共三個節點
(zk服務器集羣規模不小於3個節點),要求服務器之間系統時間保持一致。
上傳zk並且解壓
進行解壓: tar -zxvf zookeeper-3.4.6.tar.gz
重命名: mv zookeeper-3.4.6 zookeeper
修改zookeeper環境變量

vi /etc/profile
export JAVA_HOME=/opt/jdk1.8.0_71
export ZOOKEEPER_HOME=/usr/local/zookeeper
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$JAVA_HOME/bin:$ZOOKEEPER_HOME/bin:$PATH

source /etc/profile

修改zoo_sample.cfg文件

cd /usr/local/zookeeper/conf
mv zoo_sample.cfg zoo.cfg

修改conf: vi zoo.cfg 修改兩處

(1) dataDir=/usr/local/zookeeper/data(注意同時在zookeeper創建data目錄)
(2)最後面添加
server.0=bhz:2888:3888
server.1=hadoop1:2888:3888
server.2=hadoop2:2888:3888

創建服務器標識
服務器標識配置:
創建文件夾: mkdir data
創建文件myid並填寫內容爲0: vi
myid (內容爲服務器標識 : 0)
複製zookeeper
進行復制zookeeper目錄到hadoop01和hadoop02
還有/etc/profile文件
把hadoop01、 hadoop02中的myid文件裏的值修改爲1和2
路徑(vi /usr/local/zookeeper/data/myid)
啓動zookeeper
啓動zookeeper:
路徑: /usr/local/zookeeper/bin
執行: zkServer.sh start
(注意這裏3臺機器都要進行啓動)
狀態: zkServer.sh
status(在三個節點上檢驗zk的mode,一個leader和倆個follower)
常用命令
zkServer.sh status 查詢狀態
Zookeeper配置文件介紹

# The number of milliseconds of each tick 

tickTime=2000

# The number of ticks that the initial  
# synchronization phase can take 
initLimit=10 

# The number of ticks that can pass between  
# sending a request and getting an acknowledgement 

syncLimit=5

# the directory where the snapshot is stored. 
# do not use /tmp for storage, /tmp here is just  
# example sakes. 
dataDir=/home/myuser/zooA/data 

# the port at which the clients will connect 
clientPort=2181 

# ZooKeeper server and its port no. # ZooKeeper ensemble should know about every other 	machine in the ensemble # specify server id by creating 'myid' file in the dataDir # use hostname 	instead of IP address for convenient maintenance
server.1=127.0.0.1:2888:3888 
server.2=127.0.0.1:2988:3988  
server.3=127.0.0.1:2088:3088 

# 
# Be sure to read the maintenance section of the  
# administrator guide before turning on autopurge. 
# 
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance 
# 
# The number of snapshots to retain in dataDir 
# autopurge.snapRetainCount=3 
# Purge task interval in hours 
# Set to "0" to disable auto purge feature  <br>
#autopurge.purgeInterval=1 
dataLogDir=/home/myuser/zooA/log

tickTime:心跳時間,爲了確保連接存在的,以毫秒爲單位,最小超時時間爲兩個心跳時間
initLimit:多少個心跳時間內,允許其他server連接並初始化數據,如果ZooKeeper管理的數據較大,則應相應增大這個值
clientPort:服務的監聽端口
dataDir:用於存放內存數據庫快照的文件夾,同時用於集羣的myid文件也存在這個文件夾裏(注意:一個配置文件只能包含一個dataDir字樣,即使它被註釋掉了。)
dataLogDir:用於單獨設置transaction log的目錄,transaction log分離可以避免和普通log還有快照的競爭
syncLimit:多少個tickTime內,允許follower同步,如果follower落後太多,則會被丟棄。

server.A=B:C:D:
A是一個數字,表示這個是第幾號服務器,B是這個服務器的ip地址
C第一個端口用來集羣成員的信息交換,表示的是這個服務器與集羣中的Leader服務器交換信息的端口
D是在leader掛掉時專門用來進行選舉leader所用

Java操作Zookeeper

Zookeeper說明

創建節點(znode) 方法:

create:

提供了兩套創建節點的方法,同步和異步創建節點方式。
同步方式:
參數1,節點路徑《名稱) : InodeName (不允許遞歸創建節點,也就是說在父節點不存在
的情況下,不允許創建子節點)
參數2,節點內容: 要求類型是字節數組(也就是說,不支持序列化方式,如果需要實現序
列化,可使用java相關序列化框架,如Hessian、Kryo框架)
參數3,節點權限: 使用Ids.OPEN_ACL_UNSAFE開放權限即可。(這個參數一般在權展
沒有太高要求的場景下,沒必要關注)
參數4,節點類型: 創建節點的類型: CreateMode,提供四種首點象型

PERSISTENT 持久化節點
PERSISTENT_SEQUENTIAL 順序自動編號持久化節點,這種節點會根據當前已存在的節點數自動加 1
EPHEMERAL 臨時節點, 客戶端session超時這類節點就會被自動刪除
EPHEMERAL_SEQUENTIAL 臨時自動編號節點
Maven依賴信息

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

Zookeeper客戶端連接

public class Test001 {

//連接地址
private static final String ADDRES = "127.0.0.1:2181";
//session 會話
private static final int SESSION_OUTTIME = 2000;
//信號量,阻塞程序執行,用戶等待zookeeper連接成功,發送成功信號,
private static final CountDownLatch countDownLatch = new CountDownLatch(1);

public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
	ZooKeeper zk = new ZooKeeper(ADDRES, SESSION_OUTTIME, new Watcher() {

		public void process(WatchedEvent event) {
			// 獲取事件狀態
			KeeperState keeperState = event.getState();
			// 獲取事件類型
			EventType eventType = event.getType();
			if (KeeperState.SyncConnected == keeperState) {
				if (EventType.None == eventType) {
					countDownLatch.countDown();
					System.out.println("zk 啓動連接...");
				}

			}
		}
	});
	// 進行阻塞
	countDownLatch.await();
	String result = zk.create("/test_Lasting", "Lasting".getBytes(), Ids.OPEN_ACL_UNSAFE,
			CreateMode.PERSISTENT);
	System.out.println(result);
	zk.close();
	}

}

創建Zookeeper節點信息

  1. 創建持久節點,並且允許任何服務器可以操作
    String result = zk.create("/itmayiedu_Lasting", “Lasting”.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    System.out.println(“result:” + result);
  2. 創建臨時節點
    String result = zk.create("/itmayiedu_temp", “temp”.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
    System.out.println(“result:” + result);

Watcher

在ZooKeeper中,接口類Watcher用於表示一個標準的事件處理器,其定義了事件通知相關的邏輯,包含KeeperState和EventType兩個枚舉類,分別代表了通知狀態和事件類型,同時定義了事件的回調方法:process(WatchedEvent event)。

什麼是Watcher接口

同一個事件類型在不同的通知狀態中代表的含義有所不同,表7-3列舉了常見的通知狀態和事件類型。
表7-3 Watcher通知狀態與事件類型一覽
在這裏插入圖片描述

表7-3中列舉了ZooKeeper中最常見的幾個通知狀態和事件類型。
回調方法process()
process方法是Watcher接口中的一個回調方法,當ZooKeeper向客戶端發送一個Watcher事件通知時,客戶端就會對相應的process方法進行回調,從而實現對事件的處理。process方法的定義如下:
abstract public void process(WatchedEvent event);
這個回調方法的定義非常簡單,我們重點看下方法的參數定義:WatchedEvent。
WatchedEvent包含了每一個事件的三個基本屬性:通知狀態(keeperState),事件類型(EventType)和節點路徑(path),其數據結構如圖7-5所示。ZooKeeper使用WatchedEvent對象來封裝服務端事件並傳遞給Watcher,從而方便回調方法process對服務端事件進行處理。
提到WatchedEvent,不得不講下WatcherEvent實體。籠統地講,兩者表示的是同一個事物,都是對一個服務端事件的封裝。不同的是,WatchedEvent是一個邏輯事件,用於服務端和客戶端程序執行過程中所需的邏輯對象,而WatcherEvent因爲實現了序列化接口,因此可以用於網絡傳輸。
服務端在生成WatchedEvent事件之後,會調用getWrapper方法將自己包裝成一個可序列化的WatcherEvent事件,以便通過網絡傳輸到客戶端。客戶端在接收到服務端的這個事件對象後,首先會將WatcherEvent還原成一個WatchedEvent事件,並傳遞給process方法處理,回調方法process根據入參就能夠解析出完整的服務端事件了。
需要注意的一點是,無論是WatchedEvent還是WatcherEvent,其對ZooKeeper服務端事件的封裝都是機及其簡單的。舉個例子來說,當/zk-book這個節點的數據發生變更時,服務端會發送給客戶端一個“ZNode數據內容變更”事件,客戶端只能夠接收到如下信

Watcher代碼

public class ZkClientWatcher implements Watcher {
// 集羣連接地址
private static final String CONNECT_ADDRES = "192.168.110.159:2181,192.168.110.160:2181,192.168.110.162:2181";
// 會話超時時間
private static final int SESSIONTIME = 2000;
// 信號量,讓zk在連接之前等待,連接成功後才能往下走.
private static final CountDownLatch countDownLatch = new CountDownLatch(1);
private static String LOG_MAIN = "【main】 ";
private ZooKeeper zk;

public void createConnection(String connectAddres, int sessionTimeOut) {
	try {
		zk = new ZooKeeper(connectAddres, sessionTimeOut, this);
		System.out.println(LOG_MAIN + "zk 開始啓動連接服務器....");
		countDownLatch.await();
	} catch (Exception e) {
		e.printStackTrace();
	}
}

public boolean createPath(String path, String data) {
	try {
		this.exists(path, true);
		this.zk.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
		System.out.println(LOG_MAIN + "節點創建成功, Path:" + path + ",data:" + data);
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
	return true;
}

/**
 * 判斷指定節點是否存在
 * 
 * @param path
 *            節點路徑
 */
public Stat exists(String path, boolean needWatch) {
	try {
		return this.zk.exists(path, needWatch);
	} catch (Exception e) {
		e.printStackTrace();
		return null;
	}
}

public boolean updateNode(String path,String data) throws KeeperException, InterruptedException {
	exists(path, true);
	this.zk.setData(path, data.getBytes(), -1);
	return false;
}

public void process(WatchedEvent watchedEvent) {

	// 獲取事件狀態
	KeeperState keeperState = watchedEvent.getState();
	// 獲取事件類型
	EventType eventType = watchedEvent.getType();
	// zk 路徑
	String path = watchedEvent.getPath();
	System.out.println("進入到 process() keeperState:" + keeperState + ", eventType:" + eventType + ", path:" + path);
	// 判斷是否建立連接
	if (KeeperState.SyncConnected == keeperState) {
		if (EventType.None == eventType) {
			// 如果建立建立成功,讓後程序往下走
			System.out.println(LOG_MAIN + "zk 建立連接成功!");
			countDownLatch.countDown();
		} else if (EventType.NodeCreated == eventType) {
			System.out.println(LOG_MAIN + "事件通知,新增node節點" + path);
		} else if (EventType.NodeDataChanged == eventType) {
			System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被修改....");
		}
		else if (EventType.NodeDeleted == eventType) {
			System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被刪除....");
		}

	}
	System.out.println("--------------------------------------------------------");
}

public static void main(String[] args) throws KeeperException, InterruptedException {
	ZkClientWatcher zkClientWatcher = new ZkClientWatcher();
	zkClientWatcher.createConnection(CONNECT_ADDRES, SESSIONTIME);
//		boolean createResult = zkClientWatcher.createPath("/p15", "pa-644064");
	zkClientWatcher.updateNode("/pa2","7894561");
	}

}

使用Zookeeper實現負載均衡原理
思路
使用Zookeeper實現負載均衡原理,服務器端將啓動的服務註冊到,zk註冊中心上,採用臨時節點。客戶端從zk節點上獲取最新服務節點信息,本地使用負載均衡算法,隨機分配服務器。
創建項目工程
Maven依賴

	<dependencies>
	<dependency>
		<groupId>com.101tec</groupId>
		<artifactId>zkclient</artifactId>
		<version>0.8</version>
	</dependency>
</dependencies>

創建Server服務端
ZkServerScoekt服務

//##ServerScoekt服務端
public class ZkServerScoekt implements Runnable {
private int port = 18080;

public static void main(String[] args) throws IOException {
	int port = 18080;	
	ZkServerScoekt server = new ZkServerScoekt(port);
	Thread thread = new Thread(server);
	thread.start();
}

public ZkServerScoekt(int port) {
	this.port = port;
}

public void run() {
	ServerSocket serverSocket = null;
	try {
		serverSocket = new ServerSocket(port);
		System.out.println("Server start port:" + port);
		Socket socket = null;
		while (true) {
			socket = serverSocket.accept();
			new Thread(new ServerHandler(socket)).start();
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if (serverSocket != null) {
				serverSocket.close();
			}
		} catch (Exception e2) {

			}
		}
	}

}

ServerHandler

public class ServerHandler implements Runnable {
private Socket socket;

public ServerHandler(Socket socket) {
	this.socket = socket;
}

public void run() {
	BufferedReader in = null;
	PrintWriter out = null;
	try {
		in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
		out = new PrintWriter(this.socket.getOutputStream(), true);
		String body = null;
		while (true) {
			body = in.readLine();
			if (body == null)
				break;
			System.out.println("Receive : " + body);
			out.println("Hello, " + body);
		}

	} catch (Exception e) {
		if (in != null) {
			try {
				in.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
		if (out != null) {
			out.close();
		}
		if (this.socket != null) {
			try {
				this.socket.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			this.socket = null;
			}
		}
	}
}

ZkServerClient

public class ZkServerClient {
public static List<String> listServer = new ArrayList<String>();

public static void main(String[] args) {
	initServer();
	ZkServerClient 	client= new ZkServerClient();
	BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
	while (true) {
		String name;
		try {
			name = console.readLine();
			if ("exit".equals(name)) {
				System.exit(0);
			}
			client.send(name);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

// 註冊所有server
public static void initServer() {
	listServer.clear();
	listServer.add("127.0.0.1:18080");
}

// 獲取當前server信息
public static String getServer() {
	return listServer.get(0);
}

public void send(String name) {

	String server = ZkServerClient.getServer();
	String[] cfg = server.split(":");

	Socket socket = null;
	BufferedReader in = null;
	PrintWriter out = null;
	try {
		socket = new Socket(cfg[0], Integer.parseInt(cfg[1]));
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintWriter(socket.getOutputStream(), true);

		out.println(name);
		while (true) {
			String resp = in.readLine();
			if (resp == null)
				break;
			else if (resp.length() > 0) {
				System.out.println("Receive : " + resp);
				break;
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		if (out != null) {
			out.close();
		}
		if (in != null) {
			try {
				in.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (socket != null) {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
}

改造ZkServerScoekt

public class ZkServerScoekt implements Runnable {
private static int port = 18081;

public static void main(String[] args) throws IOException {
	ZkServerScoekt server = new ZkServerScoekt(port);
	Thread thread = new Thread(server);
	thread.start();
}

public ZkServerScoekt(int port) {
	this.port = port;
}

public void regServer() {
	// 向ZooKeeper註冊當前服務器
	ZkClient client = new ZkClient("127.0.0.1:2181", 60000, 1000);
	String path = "/test/server" + port;
	if (client.exists(path))
		client.delete(path);
	client.createEphemeral(path, "127.0.0.1:" + port);
}

public void run() {
	ServerSocket serverSocket = null;
	try {
		serverSocket = new ServerSocket(port);
		regServer();
		System.out.println("Server start port:" + port);
		Socket socket = null;
		while (true) {
			socket = serverSocket.accept();
			new Thread(new ServerHandler(socket)).start();
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if (serverSocket != null) {
				serverSocket.close();
			}
		} catch (Exception e2) {

		}
	}
}

}

改造ZkServerClient

public class ZkServerClient {
public static List<String> listServer = new ArrayList<String>();
public static String parent = "/test";

public static void main(String[] args) {
	initServer();
	ZkServerClient client = new ZkServerClient();
	BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
	while (true) {
		String name;
		try {
			name = console.readLine();
			if ("exit".equals(name)) {
				System.exit(0);
			}
			client.send(name);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

// 註冊所有server
public static void initServer() {
	// listServer.add("127.0.0.1:18080");

	final ZkClient zkClient = new ZkClient("127.0.0.1:2181", 6000, 1000);
	List<String> children = zkClient.getChildren(parent);
	getChilds(zkClient, children);
	// 監聽事件
	zkClient.subscribeChildChanges(parent, new IZkChildListener() {

		public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
			getChilds(zkClient, currentChilds);
		}
	});
}

private static void getChilds(ZkClient zkClient, List<String> currentChilds) {
	listServer.clear();
	for (String p : currentChilds) {
		String pathValue = (String) zkClient.readData(parent + "/" + p);
		listServer.add(pathValue);
	}
	serverCount = listServer.size();
	System.out.println("從zk讀取到信息:" + listServer.toString());

}

// 請求次數
private static int reqestCount = 1;
// 服務數量
private static int serverCount = 0;

// 獲取當前server信息
public static String getServer() {
	// 實現負載均衡
	String serverName = listServer.get(reqestCount % serverCount);
	++reqestCount;
	return serverName;
}

public void send(String name) {

	String server = ZkServerClient.getServer();
	String[] cfg = server.split(":");

	Socket socket = null;
	BufferedReader in = null;
	PrintWriter out = null;
	try {
		socket = new Socket(cfg[0], Integer.parseInt(cfg[1]));
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintWriter(socket.getOutputStream(), true);

		out.println(name);
		while (true) {
			String resp = in.readLine();
			if (resp == null)
				break;
			else if (resp.length() > 0) {
				System.out.println("Receive : " + resp);
				break;
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		if (out != null) {
			out.close();
		}
		if (in != null) {
			try {
				in.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (socket != null) {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
}

使用Zookeepers實現分佈式鎖

什麼是多線程
多線程爲了能夠提高應用程序的運行效率,在一個進程中有多條不同的執行路徑,同時並行執行,互不影響。
什麼是線程安全
當多個線程同時共享,同一個全局變量或靜態變量,做寫的操作時,可能會發生數據衝突問題,也就是線程安全問題。但是做讀操作是不會發生數據衝突問題。
解決辦法
使用同步代碼塊或者Lock鎖機制,保證在多個線程共享同一個變量只能有一個線程進行操作
什麼是Java內存模型
共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。
在這裏插入圖片描述

從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 然後,線程B到主內存中去讀取線程A之前已更新過的共享變量。
    下面通過示意圖來說明這兩個步驟:
    在這裏插入圖片描述
    如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。
    從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。
    總結:什麼是Java內存模型:java內存模型簡稱jmm,定義了一個線程對另一個線程可見。共享變量存放在主內存中,每個線程都有自己的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,所以就會發生線程安全問題。

分佈式鎖解決辦法

傳統方式生成訂單號ID
業務場景
在分佈式情況,生成全局訂單號ID
生成訂單號方案

  1. 使用時間戳
  2. 使用UUID
  3. 推特 (Twitter) 的 Snowflake 算法——用於生成唯一 ID

生成訂單類

//生成訂單類
public class OrderNumGenerator {
//全局訂單id
public static int count = 0;

public String getNumber() {
	try {
		Thread.sleep(200);
	} catch (Exception e) {
	}
	SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
	return simpt.format(new Date()) + "-" + ++count;
}
}

使用多線程情況模擬生成訂單號

//使用多線程模擬生成訂單號
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

public void run() {
	getNumber();
}

public void getNumber() {
	String number = orderNumGenerator.getNumber();
	System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
}

public static void main(String[] args) {
	System.out.println("####生成唯一訂單號###");
	for (int i = 0; i < 100; i++) {
		new Thread(new OrderService()).start();
	}

}
}

多線程生成訂單號,線程安全問題解決
使用synchronized或者loca鎖
Synchronized同步代碼塊方式

//使用多線程模擬生成訂單號
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

public void run() {
	getNumber();
}

public void getNumber() {
	synchronized (this) {
		String number = orderNumGenerator.getNumber();
		System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
	}
}

public static void main(String[] args) {
	System.out.println("####生成唯一訂單號###");
	OrderService orderService = new OrderService();
	for (int i = 0; i < 100; i++) {
		new Thread(orderService).start();
	}

}
}

Lock鎖方式

	public class OrderService implements Runnable {
	private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
	// 使用lock鎖
	private java.util.concurrent.locks.Lock lock = new ReentrantLock();

	public void run() {
		getNumber();
	}

	public void getNumber() {
		try {
			// synchronized (this) {
			lock.lock();
			String number = orderNumGenerator.getNumber();
			System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
			// }

		} catch (Exception e) {

		} finally {
		lock.unlock();
	}
}

public static void main(String[] args) {
		System.out.println("####生成唯一訂單號###");
		OrderService orderService = new OrderService();
		for (int i = 0; i < 100; i++) {
			new Thread(orderService).start();
		}

	}
}

分佈式場景下生成訂單ID

業務場景
在分佈式情況,生成全局訂單號ID
產生問題
在分佈式(集羣)環境下,每臺JVM不能實現同步,在分佈式場景下使用時間戳生成訂單號可能會重複

分佈式情況下,怎麼解決訂單號生成不重複

  1. 使用分佈式鎖
  2. 提前生成好,訂單號,存放在redis取。獲取訂單號,直接從redis中取。
    使用分佈式鎖生成訂單號技術
    1.使用數據庫實現分佈式鎖
    缺點:性能差、線程出現異常時,容易出現死鎖
    2.使用redis實現分佈式鎖
    缺點:鎖的失效時間難控制、容易產生死鎖、非阻塞式、不可重入
    3.使用zookeeper實現分佈式鎖
    實現相對簡單、可靠性強、使用臨時節點,失效時間容易控制
    什麼是分佈式鎖
    分佈式鎖一般用在分佈式系統或者多個應用中,用來控制同一任務是否執行或者任務的執行順序。在項目中,部署了多個tomcat應用,在執行定時任務時就會遇到同一任務可能執行多次的情況,我們可以藉助分佈式鎖,保證在同一時間只有一個tomcat應用執行了定時任務

使用Zookeeper實現分佈式鎖
Zookeeper實現分佈式鎖原理
使用zookeeper創建臨時序列節點來實現分佈式鎖,適用於順序執行的程序,大體思路就是創建臨時序列節點,找出最小的序列節點,獲取分佈式鎖,程序執行完成之後此序列節點消失,通過watch來監控節點的變化,從剩下的節點的找到最小的序列節點,獲取分佈式鎖,執行相應處理,依次類推……
Maven依賴

<dependencies>
	<dependency>
		<groupId>com.101tec</groupId>
		<artifactId>zkclient</artifactId>
		<version>0.10</version>
	</dependency>
</dependencies>

創建Lock接口

	public interface Lock {
	 //獲取到鎖的資源
	public void getLock();
	 // 釋放鎖
	public void unLock();
	}

創建ZookeeperAbstractLock抽象類

//將重複代碼寫入子類中…

public abstract class ZookeeperAbstractLock implements Lock {
// zk連接地址
private static final String CONNECTSTRING = "127.0.0.1:2181";
// 創建zk連接
protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
protected static final String PATH = "/lock";

public void getLock() {
	if (tryLock()) {
		System.out.println("##獲取lock鎖的資源####");
	} else {
		// 等待
		waitLock();
		// 重新獲取鎖資源
		getLock();
	}

}

// 獲取鎖資源
abstract boolean tryLock();

// 等待
abstract void waitLock();

public void unLock() {
	if (zkClient != null) {
		zkClient.close();
		System.out.println("釋放鎖資源...");
	}
}

}

ZookeeperDistrbuteLock類

public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch = null;

@Override
boolean tryLock() {
	try {
		zkClient.createEphemeral(PATH);
		return true;
	} catch (Exception e) {
//			e.printStackTrace();
		return false;
	}

}

@Override
void waitLock() {
	IZkDataListener izkDataListener = new IZkDataListener() {

		public void handleDataDeleted(String path) throws Exception {
			// 喚醒被等待的線程
			if (countDownLatch != null) {
				countDownLatch.countDown();
			}
		}
		public void handleDataChange(String path, Object data) throws Exception {

		}
	};
	// 註冊事件
	zkClient.subscribeDataChanges(PATH, izkDataListener);
	if (zkClient.exists(PATH)) {
		countDownLatch = new CountDownLatch(1);
		try {
			countDownLatch.await();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	// 刪除監聽
	zkClient.unsubscribeDataChanges(PATH, izkDataListener);
}

}

使用Zookeeper鎖運行效果

public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
// 使用lock鎖
// private java.util.concurrent.locks.Lock lock = new ReentrantLock();
private Lock lock = new ZookeeperDistrbuteLock();
public void run() {
	getNumber();
}
public void getNumber() {
	try {
		lock.getLock();
		String number = orderNumGenerator.getNumber();
		System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		lock.unLock();
	}
}
public static void main(String[] args) {
	System.out.println("####生成唯一訂單號###");
	//OrderService orderService = new OrderService();
	for (int i = 0; i < 100; i++) {
		new Thread( new OrderService()).start();
	}
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章