點關注,不迷路!如果本文對你有幫助的話不要忘記點贊支持哦!
Zookeeper 分布式服務框架是 Apache Hadoop 的一個子專案,它主要是用來解決分布式應用中經常遇到的一些資料管理問題,如:統一命名服務、狀態同步服務、集群管理、分布式應用配置項的管理等,本文將從使用者角度詳細介紹 Zookeeper 的安裝和組態檔中各個配置項的意義,以及分析 Zookeeper 的典型的應用場景(組態檔的管理、集群管理、同步鎖、Leader 選舉、佇列管理等),用 Java 實作它們并給出示例代碼,
安裝和配置詳解
本文介紹的 Zookeeper 是以 3.2.2 這個穩定版本為基礎,最新的版本可以通過官網 http://zookeeper.apache.org/來獲取,Zookeeper 的安裝非常簡單,下面將從單機模式和集群模式兩個方面介紹 Zookeeper 的安裝和配置,
單機模式
單機安裝非常簡單,只要獲取到 Zookeeper 的壓縮包并解壓到某個目錄如:/home/zookeeper-3.2.2 下,Zookeeper 的啟動腳本在 bin 目錄下,Linux 下的啟動腳本是 zkServer.sh,在 3.2.2 這個版本 Zookeeper 沒有提供 windows 下的啟動腳本,所以要想在 windows 下啟動 Zookeeper 要自己手工寫一個,如清單 1 所示:
清單 1. Windows 下 Zookeeper 啟動腳本
setlocal
set ZOOCFGDIR=%~dp0%..\conf
set ZOO_LOG_DIR=%~dp0%..
set ZOO_LOG4J_PROP=INFO,CONSOLE
set CLASSPATH=%ZOOCFGDIR%
set CLASSPATH=%~dp0..\*;%~dp0..\lib\*;%CLASSPATH%
set CLASSPATH=%~dp0..\build\classes;%~dp0..\build\lib\*;%CLASSPATH%
set ZOOCFG=%ZOOCFGDIR%\zoo.cfg
set ZOOMAIN=org.apache.zookeeper.server.ZooKeeperServerMain
java "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%"
-cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
endlocal
在你執行啟動腳本之前,還有幾個基本的配置項需要配置一下,Zookeeper 的組態檔在 conf 目錄下,這個目錄下有 zoo_sample.cfg 和 log4j.properties,你需要做的就是將 zoo_sample.cfg 改名為 zoo.cfg,因為 Zookeeper 在啟動時會找這個檔案作為默認組態檔,下面詳細介紹一下,這個組態檔中各個配置項的意義,
tickTime=2000
dataDir=D:/devtools/zookeeper-3.2.2/build
clientPort=2181
- tickTime:這個時間是作為 Zookeeper 服務器之間或客戶端與服務器之間維持心跳的時間間隔,也就是每個 tickTime 時間就會發送一個心跳,
- dataDir:顧名思義就是 Zookeeper 保存資料的目錄,默認情況下,Zookeeper 將寫資料的日志檔案也保存在這個目錄里,
- clientPort:這個埠就是客戶端連接 Zookeeper 服務器的埠,Zookeeper 會監聽這個埠,接受客戶端的訪問請求,
當這些配置項配置好后,你現在就可以啟動 Zookeeper 了,啟動后要檢查 Zookeeper 是否已經在服務,可以通過 netstat – ano 命令查看是否有你配置的 clientPort 埠號在監聽服務,
集群模式
Zookeeper 不僅可以單機提供服務,同時也支持多機組成集群來提供服務,實際上 Zookeeper 還支持另外一種偽集群的方式,也就是可以在一臺物理機上運行多個 Zookeeper 實體,下面將介紹集群模式的安裝和配置,
Zookeeper 的集群模式的安裝和配置也不是很復雜,所要做的就是增加幾個配置項,集群模式除了上面的三個配置項還要增加下面幾個配置項:
initLimit=5
syncLimit=2
server.1=192.168.211.1:2888:3888
server.2=192.168.211.2:2888:3888
- initLimit:這個配置項是用來配置 Zookeeper 接受客戶端(這里所說的客戶端不是用戶連接 Zookeeper 服務器的客戶端,而是 Zookeeper 服務器集群中連接到 Leader 的 Follower 服務器)初始化連接時最長能忍受多少個心跳時間間隔數,當已經超過 10 個心跳的時間(也就是 tickTime)長度后 Zookeeper 服務器還沒有收到客戶端的回傳資訊,那么表明這個客戶端連接失敗,總的時間長度就是 5*2000=10 秒
- syncLimit:這個配置項標識 Leader 與 Follower 之間發送訊息,請求和應答時間長度,最長不能超過多少個 tickTime 的時間長度,總的時間長度就是 2*2000=4 秒
- server.A=B:C:D:其中 A 是一個數字,表示這個是第幾號服務器;B 是這個服務器的 ip 地址;C 表示的是這個服務器與集群中的 Leader 服務器交換資訊的埠;D 表示的是萬一集群中的 Leader 服務器掛了,需要一個埠來重新進行選舉,選出一個新的 Leader,而這個埠就是用來執行選舉時服務器相互通信的埠,如果是偽集群的配置方式,由于 B 都是一樣,所以不同的 Zookeeper 實體通信埠號不能一樣,所以要給它們分配不同的埠號,
除了修改 zoo.cfg 組態檔,集群模式下還要配置一個檔案 myid,這個檔案在 dataDir 目錄下,這個檔案里面就有一個資料就是 A 的值,Zookeeper 啟動時會讀取這個檔案,拿到里面的資料與 zoo.cfg 里面的配置資訊比較從而判斷到底是那個 server,
資料模型
Zookeeper 會維護一個具有層次關系的資料結構,它非常類似于一個標準的檔案系統,如圖 1 所示:
圖 1 Zookeeper 資料結構

Paste_Image.png
Zookeeper 這種資料結構有如下這些特點:
- 每個子目錄項如 NameService 都被稱作為 znode,這個 znode 是被它所在的路徑唯一標識,如 Server1 這個 znode 的標識為 /NameService/Server1
- znode 可以有子節點目錄,并且每個 znode 可以存盤資料,注意 EPHEMERAL 型別的目錄節點不能有子節點目錄
- znode 是有版本的,每個 znode 中存盤的資料可以有多個版本,也就是一個訪問路徑中可以存盤多份資料
- znode 可以是臨時節點,一旦創建這個 znode 的客戶端與服務器失去聯系,這個 znode 也將自動洗掉,Zookeeper 的客戶端和服務器通信采用長連接方式,每個客戶端和服務器通過心跳來保持連接,這個連接狀態稱為 session,如果 znode 是臨時節點,這個 session 失效,znode 也就洗掉了
- znode 的目錄名可以自動編號,如 App1 已經存在,再創建的話,將會自動命名為 App2
- znode 可以被監控,包括這個目錄節點中存盤的資料的修改,子節點目錄的變化等,一旦變化可以通知設定監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于這個特性實作的,后面在典型的應用場景中會有實體介紹
如何使用
Zookeeper 作為一個分布式的服務框架,主要用來解決分布式集群中應用系統的一致性問題,它能提供基于類似于檔案系統的目錄節點樹方式的資料存盤,但是 Zookeeper 并不是用來專門存盤資料的,它的作用主要是用來維護和監控你存盤的資料的狀態變化,通過監控這些資料狀態的變化,從而可以達到基于資料的集群管理,后面將會詳細介紹 Zookeeper 能夠解決的一些典型問題,這里先介紹一下,Zookeeper 的操作介面和簡單使用示例,
常用介面串列
客戶端要連接 Zookeeper 服務器可以通過創建 org.apache.zookeeper. ZooKeeper 的一個實體物件,然后呼叫這個類提供的介面來和服務器互動,
前面說了 ZooKeeper主要是用來維護和監控一個目錄節點樹中存盤的資料的狀態,所有我們能夠操作ZooKeeper的也和操作目錄節點樹大體一樣,如創建一個目錄節點,給某個目錄節點設定資料,獲取某個目錄節點的所有子目錄節點,給某個目錄節點設定權限和監控這個目錄節點的狀態變化, 這些介面如下表所示:
表 1 org.apache.zookeeper. ZooKeeper 方法串列
方法名
方法功能描述
String create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
創建一個給定的目錄節點 path, 并給它設定資料,CreateMode 標識有四種形式的目錄節點,分別是 PERSISTENT:持久化目錄節點,這個目錄節點存盤的資料不會丟失;PERSISTENT_SEQUENTIAL:順序自動編號的目錄節點,這種目錄節點會根據當前已近存在的節點數自動加 1,然后回傳給客戶端已經成功創建的目錄節點名;EPHEMERAL:臨時目錄節點,一旦創建這個節點的客戶端與服務器埠也就是 session 超時,這種節點會被自動洗掉;EPHEMERAL_SEQUENTIAL:臨時自動編號節點
<br >
Stat exists(String path, boolean watch)
判斷某個 path 是否存在,并設定是否監控這個目錄節點,這里的 watcher 是在創建 ZooKeeper 實體時指定的 watcher,exists方法還有一個多載方法,可以指定特定的 watcher
<br >
Stat exists(String path, Watcher watcher)
多載方法,這里給某個目錄節點設定特定的 watcher,Watcher 在 ZooKeeper 是一個核心功能,Watcher 可以監控目錄節點的資料變化以及子目錄的變化,一旦這些狀態發生變化,服務器就會通知所有設定在這個目錄節點上的 Watcher,從而每個客戶端都很快知道它所關注的目錄節點的狀態發生變化,而做出相應的反應
<br >
void delete(String path, int version)
洗掉 path 對應的目錄節點,version 為 -1 可以匹配任何版本,也就洗掉了這個目錄節點所有資料
<br >
List<String> getChildren(String path, boolean watch)
獲取指定 path 下的所有子目錄節點,同樣 getChildren方法也有一個多載方法可以設定特定的 watcher 監控子節點的狀態
<br >
Stat setData(String path, byte[] data, int version)
給 path 設定資料,可以指定這個資料的版本號,如果 version 為 -1 怎可以匹配任何版本
<br >
byte[] getData(String path, boolean watch, Stat stat)
獲取這個 path 對應的目錄節點存盤的資料,資料的版本等資訊可以通過 stat 來指定,同時還可以設定是否監控這個目錄節點資料的狀態
<br >
void addAuthInfo(String scheme, byte[] auth)
客戶端將自己的授權資訊提交給服務器,服務器將根據這個授權資訊驗證客戶端的訪問權限,
<br >
Stat setACL(String path, List<ACL> acl, int version)
給某個目錄節點重新設定訪問權限,需要注意的是 Zookeeper 中的目錄節點權限不具有傳遞性,父目錄節點的權限不能傳遞給子目錄節點,目錄節點 ACL 由兩部分組成:perms 和 id,
Perms 有 ALL、READ、WRITE、CREATE、DELETE、ADMIN 幾種
而 id 標識了訪問目錄節點的身份串列,默認情況下有以下兩種:
ANYONE_ID_UNSAFE = new Id("world", "anyone") 和 AUTH_IDS = new Id("auth", "") 分別表示任何人都可以訪問和創建者擁有訪問權限,
<br >
List<ACL> getACL(String path, Stat stat)
獲取某個目錄節點的訪問權限串列
除了以上這些上表中列出的方法之外還有一些多載方法,如都提供了一個回呼類的多載方法以及可以設定特定 Watcher 的多載方法,具體的方法可以參考 org.apache.zookeeper. ZooKeeper 類的 API 說明,
基本操作
下面給出基本的操作 ZooKeeper 的示例代碼,這樣你就能對 ZooKeeper 有直觀的認識了,下面的清單包括了創建與 ZooKeeper 服務器的連接以及最基本的資料操作:
清單 2. ZooKeeper 基本的操作示例
// 創建一個與服務器的連接
ZooKeeper zk = new ZooKeeper("localhost:" + CLIENT_PORT,
ClientBase.CONNECTION_TIMEOUT, new Watcher() {
// 監控所有被觸發的事件
public void process(WatchedEvent event) {
System.out.println("已經觸發了" + event.getType() + "事件!");
}
});
// 創建一個目錄節點
zk.create("/testRootPath", "testRootData".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
// 創建一個子目錄節點
zk.create("/testRootPath/testChildPathOne", "testChildDataOne".getBytes(),
Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath",false,null)));
// 取出子目錄節點串列
System.out.println(zk.getChildren("/testRootPath",true));
// 修改子目錄節點資料
zk.setData("/testRootPath/testChildPathOne","modifyChildDataOne".getBytes(),-1);
System.out.println("目錄節點狀態:["+zk.exists("/testRootPath",true)+"]");
// 創建另外一個子目錄節點
zk.create("/testRootPath/testChildPathTwo", "testChildDataTwo".getBytes(),
Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath/testChildPathTwo",true,null)));
// 洗掉子目錄節點
zk.delete("/testRootPath/testChildPathTwo",-1);
zk.delete("/testRootPath/testChildPathOne",-1);
// 洗掉父目錄節點
zk.delete("/testRootPath",-1);
// 關閉連接
zk.close();
輸出的結果如下:
已經觸發了 None 事件!
testRootData
[testChildPathOne]
目錄節點狀態:[5,5,1281804532336,1281804532336,0,1,0,0,12,1,6]
已經觸發了 NodeChildrenChanged 事件!
testChildDataTwo
已經觸發了 NodeDeleted 事件!
已經觸發了 NodeDeleted 事件!
當對目錄節點監控狀態打開時,一旦目錄節點的狀態發生變化,Watcher 物件的 process 方法就會被呼叫,

上述面試題答案都整理成檔案筆記, 也還整理了一些面試資料&最新2020收集的一些大廠的面試真題(都整理成檔案,小部分截圖),有需要的可以 點擊進入暗號:csdn ,
ZooKeeper 典型的應用場景
Zookeeper 從設計模式角度來看,是一個基于觀察者模式設計的分布式服務管理框架,它負責存盤和管理大家都關心的資料,然后接受觀察者的注冊,一旦這些資料的狀態發生變化,Zookeeper 就將負責通知已經在 Zookeeper 上注冊的那些觀察者做出相應的反應,從而實作集群中類似 Master/Slave 管理模式,關于 Zookeeper 的詳細架構等內部細節可以閱讀 Zookeeper 的原始碼
下面詳細介紹這些典型的應用場景,也就是 Zookeeper 到底能幫我們解決那些問題?下面將給出答案,
統一命名服務(Name Service)
分布式應用中,通常需要有一套完整的命名規則,既能夠產生唯一的名稱又便于人識別和記住,通常情況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重復,說到這里你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 能夠完成的功能是差不多的,它們都是將有層次的目錄結構關聯到一定資源上,但是 Zookeeper 的 Name Service 更加是廣泛意義上的關聯,也許你并不需要將名稱關聯到特定資源上,你可能只需要一個不會重復名稱,就像資料庫中產生一個唯一的數字主鍵一樣,
Name Service 已經是 Zookeeper 內置的功能,你只要呼叫 Zookeeper 的 API 就能實作,如呼叫 create 介面就可以很容易創建一個目錄節點,
配置管理(Configuration Management)
配置的管理在分布式應用環境中很常見,例如同一個應用系統需要多臺 PC Server 運行,但是它們運行的應用系統的某些配置項是相同的,如果要修改這些相同的配置項,那么就必須同時修改每臺運行這個應用系統的 PC Server,這樣非常麻煩而且容易出錯,
像這樣的配置資訊完全可以交給 Zookeeper 來管理,將配置資訊保存在 Zookeeper 的某個目錄節點中,然后將所有需要修改的應用機器監控配置資訊的狀態,一旦配置資訊發生變化,每臺應用機器就會收到 Zookeeper 的通知,然后從 Zookeeper 獲取新的配置資訊應用到系統中,
圖 2. 配置管理結構圖

Paste_Image.png
集群管理(Group Membership)
Zookeeper 能夠很容易的實作集群管理的功能,如有多臺 Server 組成一個服務集群,那么必須要一個“總管”知道當前集群中每臺機器的服務狀態,一旦有機器不能提供服務,集群中其它集群必須知道,從而做出調整重新分配服務策略,同樣當增加集群的服務能力時,就會增加一臺或多臺 Server,同樣也必須讓“總管”知道,
Zookeeper 不僅能夠幫你維護當前的集群中機器的服務狀態,而且能夠幫你選出一個“總管”,讓這個總管來管理集群,這就是 Zookeeper 的另一個功能 Leader Election,
它們的實作方式都是在 Zookeeper 上創建一個 EPHEMERAL 型別的目錄節點,然后每個 Server 在它們創建目錄節點的父目錄節點上呼叫 getChildren(String path, boolean watch) 方法并設定 watch 為 true,由于是 EPHEMERAL 目錄節點,當創建它的 Server 死去,這個目錄節點也隨之被洗掉,所以 Children 將會變化,這時 getChildren上的 Watch 將會被呼叫,所以其它 Server 就知道已經有某臺 Server 死去了,新增 Server 也是同樣的原理,
Zookeeper 如何實作 Leader Election,也就是選出一個 Master Server,和前面的一樣每臺 Server 創建一個 EPHEMERAL 目錄節點,不同的是它還是一個 SEQUENTIAL 目錄節點,所以它是個 EPHEMERAL_SEQUENTIAL 目錄節點,之所以它是 EPHEMERAL_SEQUENTIAL 目錄節點,是因為我們可以給每臺 Server 編號,我們可以選擇當前是最小編號的 Server 為 Master,假如這個最小編號的 Server 死去,由于是 EPHEMERAL 節點,死去的 Server 對應的節點也被洗掉,所以當前的節點串列中又出現一個最小編號的節點,我們就選擇這個節點為當前 Master,這樣就實作了動態選擇 Master,避免了傳統意義上單 Master 容易出現單點故障的問題,
圖 3. 集群管理結構圖

Paste_Image.png
這部分的示例代碼如下,完整的代碼請看附件:
清單 3. Leader Election 關鍵代碼
void findLeader() throws InterruptedException {
byte[] leader = null;
try {
leader = zk.getData(root + "/leader", true, null);
} catch (Exception e) {
logger.error(e);
}
if (leader != null) {
following();
} else {
String newLeader = null;
try {
byte[] localhost = InetAddress.getLocalHost().getAddress();
newLeader = zk.create(root + "/leader", localhost,
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e) {
logger.error(e);
}
if (newLeader != null) {
leading();
} else {
mutex.wait();
}
}
}
共享鎖(Locks)
共享鎖在同一個行程中很容易實作,但是在跨行程或者在不同 Server 之間就不好實作了,Zookeeper 卻很容易實作這個功能,實作方式也是需要獲得鎖的 Server 創建一個 EPHEMERAL_SEQUENTIAL 目錄節點,然后呼叫 getChildren方法獲取當前的目錄節點串列中最小的目錄節點是不是就是自己創建的目錄節點,如果正是自己創建的,那么它就獲得了這個鎖,如果不是那么它就呼叫 exists(String path, boolean watch) 方法并監控 Zookeeper 上目錄節點串列的變化,一直到自己創建的節點是串列中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要洗掉前面它自己所創建的目錄節點就行了,
圖 4. Zookeeper 實作 Locks 的流程圖

Paste_Image.png
同步鎖的實作代碼如下,完整的代碼請看附件:
清單 4. 同步鎖的關鍵代碼
void getLock() throws KeeperException, InterruptedException{
List<String> list = zk.getChildren(root, false);
String[] nodes = list.toArray(new String[list.size()]);
Arrays.sort(nodes);
if(myZnode.equals(root+"/"+nodes[0])){
doAction();
}
else{
waitForLock(nodes[0]);
}
}
void waitForLock(String lower) throws InterruptedException, KeeperException {
Stat stat = zk.exists(root + "/" + lower,true);
if(stat != null){
mutex.wait();
}
else{
getLock();
}
}
佇列管理
Zookeeper 可以處理兩種型別的佇列:
- 當一個佇列的成員都聚齊時,這個佇列才可用,否則一直等待所有成員到達,這種是同步佇列,
- 佇列按照 FIFO 方式進行入隊和出隊操作,例如實作生產者和消費者模型,
同步佇列用 Zookeeper 實作的實作思路如下:
創建一個父目錄 /synchronizing,每個成員都監控標志(Set Watch)位目錄 /synchronizing/start 是否存在,然后每個成員都加入這個佇列,加入佇列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然后每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i,判斷 i 的值是否已經是成員的個數,如果小于成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start,
用下面的流程圖更容易理解:

Paste_Image.png
同步佇列的關鍵代碼如下,完整的代碼請看附件:
清單 5. 同步佇列
void addQueue() throws KeeperException, InterruptedException{
zk.exists(root + "/start",true);
zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
synchronized (mutex) {
List<String> list = zk.getChildren(root, false);
if (list.size() < size) {
mutex.wait();
} else {
zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
}
}
當佇列沒滿是進入 wait(),然后會一直等待 Watch 的通知,Watch 的代碼如下:
public void process(WatchedEvent event) {
if(event.getPath().equals(root + "/start") &&
event.getType() == Event.EventType.NodeCreated){
System.out.println("得到通知");
super.process(event);
doAction();
}
}
FIFO 佇列用 Zookeeper 實作思路如下:
實作的思路也非常簡單,就是在特定的目錄下創建 SEQUENTIAL 型別的子目錄 /queue_i,這樣就能保證所有成員加入佇列時都是有編號的,出佇列時通過 getChildren( ) 方法可以回傳當前所有的佇列中的元素,然后消費其中最小的一個,這樣就能保證 FIFO,
下面是生產者和消費者這種佇列形式的示例代碼,完整的代碼請看附件:
清單 6. 生產者代碼
boolean produce(int i) throws KeeperException, InterruptedException{
ByteBuffer b = ByteBuffer.allocate(4);
byte[] value;
b.putInt(i);
value = b.array();
zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
return true;
}
清單 7. 消費者代碼
int consume() throws KeeperException, InterruptedException{
int retvalue = -1;
Stat stat = null;
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
if (list.size() == 0) {
mutex.wait();
} else {
Integer min = new Integer(list.get(0).substring(7));
for(String s : list){
Integer tempValue = new Integer(s.substring(7));
if(tempValue < min) min = tempValue;
}
byte[] b = zk.getData(root + "/element" + min,false, stat);
zk.delete(root + "/element" + min, 0);
ByteBuffer buffer = ByteBuffer.wrap(b);
retvalue = buffer.getInt();
return retvalue;
}
}
}
}
總結
Zookeeper 作為 Hadoop 專案中的一個子專案,是 Hadoop 集群管理的一個必不可少的模塊,它主要用來控制集群中的資料,如它管理 Hadoop 集群中的 NameNode,還有 Hbase 中 Master Election、Server 之間狀態同步等,
本文介紹的 Zookeeper 的基本知識,以及介紹了幾個典型的應用場景,這些都是 Zookeeper 的基本功能,最重要的是 Zoopkeeper 提供了一套很好的分布式集群管理的機制,就是它這種基于層次型的目錄樹的資料結構,并對樹中的節點進行有效管理,從而可以設計出多種多樣的分布式的資料管理模型,而不僅僅局限于上面提到的幾個常用應用場景,
到此這篇關于文章就結束了!
點關注,不迷路!如果本文對你有幫助的話不要忘記點贊支持哦!

上述面試題答案都整理成檔案筆記, 也還整理了一些面試資料&最新2020收集的一些大廠的面試真題(都整理成檔案,小部分截圖),有需要的可以 點擊進入暗號:csdn ,
希望對大家有所幫助,有用的話點贊給我支持!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/41670.html
標籤:其他
