- 命名服務是爲系統中的資源提供標識能力。ZooKeeper的命名服務主要是利用ZooKeeper節點的樹形分層結構和子節點的順序維護能力,來爲分佈式系統中的資源命名。
- 分佈式命名服務器應用場景:
- 分佈式API目錄:爲分佈式系統中各種API接口服務的名稱、鏈接地址,提供類似JNDI(Java命名和目錄接口)中的文件系統的功能。藉助於ZooKeeper的樹形分層結構就能提供分佈式的API調用功能。著名的Dubbo分佈式框架就是應用了ZooKeeper的分佈式的JNDI功能。在Dubbo中,使用ZooKeeper維護的全局服務接口API的地址列表。
- 分佈式ID生成器:在分佈式系統中,爲每一個數據資源提供唯一性的ID標識功能。
- 分佈式節點命名。
分佈式的ID生成器方案
- Java的UUID。UUID是經由一定的算法機器生成的,爲了保證UUID的唯一性,規範定義了包括網卡MAC地址、時間戳、名字空間(Namespace)、隨機或僞隨機數、時序等元素,以及從這些元素生成UUID的算法。一個UUID是16字節長的數字,一共128位。轉成字符串之後,它會變成一個36字節的字符串。
- UUID的優點是本地生成ID,不需要進行遠程調用,時延低,性能高。
- UUID的缺點是UUID過長,16字節共128位,通常以36字節長的字符串來表示,在很多應用場景不適用,例如,由於UUID沒有排序,無法保證趨勢遞增,因此用於數據庫索引字段的效率就很低,添加記錄存儲入庫時性能差。
- 分佈式緩存Redis生成ID:利用Redis的原子操作INCR和INCRBY,生成全局唯一的ID。
- Twitter的SnowFlake算法。
- ZooKeeper生成ID:利用ZooKeeper的順序節點,生成全局唯一的ID。
- MongoDb的ObjectId:MongoDB是一個分佈式的非結構化NoSQL數據庫,每插入一條記錄會自動生成全局唯一的一個“_id”字段值,它是一個12字節的字符串,可以作爲分佈式系統中全局唯一的ID。
ZooKeeper分佈式ID生成器實踐:ZooKeeper的每一個節點都會爲它的第一級子節點維護一份順序編號,會記錄每個子節點創建的先後順序,這個順序編號是分佈式同步的,也是全局唯一的。節點創建完成後,會返回節點的完整路徑,生成的序號放置在路徑的末尾,一般爲10位數字字符。可以通過截取路徑尾部數字作爲新生成的ID。
public class IDMaker { CuratorFramework client = null; public void init() { //創建客戶端 client = ClientFactory.createSimple(“127.0.0.1:2181”); //啓動客戶端實例,連接服務器 client.start(); } public void destroy() { if (null != client) { client.close(); } /** *創建臨時順序節點 *@param pathPefix *@return 創建後的完整路徑名稱 private String createSeqNode(String pathPefix) { try { // 創建一個 ZNode 順序節點,避免zookeeper的順序節點暴增,需要刪除創建的持久化順序節點 String destPath = client.create().creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(pathPefix); return destPath; } catch (Exception e) { e.printStackTrace(); } return null; } //創建ID public String makeId(String nodeName) { String str = createSeqNode(nodeName); if (null == str) { return null; } int index = str.lastIndexOf(nodeName); if (index >= 0) { index += nodeName.length(); return index <= str.length() ? str.substring(index) : ""; } return str; } } 測試用例 @Slf4j public class IDMakerTester { @Test public void testMakeId() { IDMaker idMaker = new IDMaker(); idMaker.init(); String nodeName = "/test/IDMaker/ID-"; for (int i = 0; i < 10; i++) { String id = idMaker.makeId(nodeName); log.info("第" + i + "個創建的id爲:" + id); } idMaker.destroy(); } } |
集羣節點的命名服務實踐
有以下兩個方案可用於生成集羣節點的編號:
(1)使用數據庫的自增ID特性,用數據表存儲機器的MAC地址或者IP來維護。
(2)使用ZooKeeper持久順序節點的順序特性來維護節點的NodeId編號。
使用ZooKeeper集羣節點命名服務的基本流程是:
- ·啓動節點服務,連接ZooKeeper,檢查命名服務根節點是否存在,如果不存在,就創建系統的根節點。
- ·在根節點下創建一個臨時順序ZNode節點,取回ZNode的編號把它作爲分佈式系統中節點的NODEID。·如果臨時節點太多,可以根據需要刪除臨時順序ZNode節點。
使用ZK實現SnowFlakeID算法實踐
SnowFlake算法所生成的ID是一個64bit的長整型數字。這個64bit被劃分成四個部分,其中後面三個部分分別表示時間戳、工作機器ID、序列號。SnowFlakeID的四個部分,具體介紹如下:
(1)第一位 佔用1 bit,其值始終是0,沒有實際作用。
(2)時間戳 佔用41 bit,精確到毫秒,總共可以容納約69年的時間。
(3)工作機器id 佔用10 bit,最多可以容納1024個節點。
(4)序列號 佔用12 bit,最多可以累加到4095。這個值在同一毫秒同一節點上從0開始不斷累加。
public class SnowflakeIdGenerator {
/** * 單例 */ public static SnowflakeIdGenerator instance = new SnowflakeIdGenerator();
/** * 初始化單例 * @param workerId 節點Id,最大8091 * @return the 單例 */ public synchronized void init(long workerId) { if (workerId > MAX_WORKER_ID) { // zk分配的workerId過大 throw new IllegalArgumentException("woker Id wrong: " + workerId); } instance.workerId = workerId; } private SnowflakeIdGenerator() { } /** * 開始使用該算法的時間爲: 2017-01-01 00:00:00 */ private static final long START_TIME = 1483200000000L; /** * worker id 的bit數,最多支持8192個節點 */ private static final int WORKER_ID_BITS = 13; /** * 序列號,支持單節點最高每毫秒的最大ID數1024 */ private final static int SEQUENCE_BITS = 10; /** * 最大的 worker id ,8091 * -1 的補碼(二進制全1)右移13位, 然後取反 */ private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); /** * 最大的序列號,1023 * -1 的補碼(二進制全1)右移10位, 然後取反 */ private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); /** * worker 節點編號的移位 */ private final static long APP_HOST_ID_SHIFT = SEQUENCE_BITS; /** * 時間戳的移位 */ private final static long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + APP_HOST_ID_SHIFT; /** * 該項目的worker 節點 id */ private long workerId; /** * 上次生成ID的時間戳 */ private long lastTimestamp = -1L; /** * 當前毫秒生成的序列 */ private long sequence = 0L; /** * Next id long. * * @return the nextId */ public Long nextId() { return generateId(); } /** * 生成唯一id的具體實現 */ private synchronized long generateId() { long current = System.currentTimeMillis(); if (current < lastTimestamp) { // 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過,出現問題返回-1 return -1; } if (current == lastTimestamp) { // 如果當前生成id的時間還是上次的時間,那麼對sequence序列號進行+1 sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == MAX_SEQUENCE) { // 當前毫秒生成的序列數已經大於最大值,那麼阻塞到下一個毫秒再獲取新的時間戳 current = this.nextMs(lastTimestamp); } } else { // 當前的時間戳已經是下一個毫秒 sequence = 0L; } // 更新上次生成id的時間戳 lastTimestamp = current; // 進行移位操作生成int64的唯一ID //時間戳右移動23位 long time = (current - START_TIME) << TIMESTAMP_LEFT_SHIFT; //workerId 右移動10位 long workerId = this.workerId << APP_HOST_ID_SHIFT; return time | workerId | sequence; }
/** * 阻塞到下一個毫秒 */ private long nextMs(long timeStamp) { long current = System.currentTimeMillis(); while (current <= timeStamp) { current = System.currentTimeMillis(); } return current; } } |
@Slf4j public class SnowflakeIdTest { /** * The entry point of application. * * @param args the input arguments * @throws InterruptedException the interrupted exception */ public static void main(String[] args) throws InterruptedException { SnowflakeIdGenerator.instance.init(SnowflakeIdWorker.instance.getId()); ExecutorService es = Executors.newFixedThreadPool(10); final HashSet idSet = new HashSet(); Collections.synchronizedCollection(idSet); long start = System.currentTimeMillis(); log.info(" start generate id *"); for (int i = 0; i < 10; i++) es.execute(() -> { for (long j = 0; j < 5000000; j++) { long id = SnowflakeIdGenerator.instance.nextId(); synchronized (idSet) { idSet.add(id); } } }); es.shutdown(); es.awaitTermination(10, TimeUnit.SECONDS); long end = System.currentTimeMillis(); log.info(" end generate id "); log.info("* cost " + (end - start) + " ms!"); } } |
SnowFlake算法的優點:
- 生成ID時不依賴於數據庫,完全在內存生成,高性能和高可用性。
- 容量大,每秒可生成幾百萬個ID。
- ID呈趨勢遞增,後續插入數據庫的索引樹時,性能較高。
SnowFlake算法的缺點:
- 依賴於系統時鐘的一致性,如果某臺機器的系統時鐘回撥了,有可能造成ID衝突,或者ID亂序。
- 在啓動之前,如果這臺機器的系統時間回撥過,那麼有可能出現ID重複的危險。
ZooKeeper分佈式鎖
ZooKeeper的臨時順序節點,可以實現分佈式鎖的原因:
1) ZooKeeper的每一個節點都是一個天然的順序發號器。
2) ZooKeeper節點的遞增有序性可以確保鎖的公平。一個ZooKeeper分佈式鎖,首先需要創建一個父節點,儘量是持久節點(PERSISTENT類型),然後每個要獲得鎖的線程都在這個節點下創建個臨時順序節點。
3) ZooKeeper的節點監聽機制可以保障佔有鎖的傳遞有序而且高效。ZooKeeper內部優越的機制,能保證由於網絡異常或者其他原因造成集羣中佔用鎖的客戶端失聯時,鎖能夠被有效釋放。
4)ZooKeeper的節點監聽機制能避免羊羣效應。
- 實例
public interface Lock { boolean lock() throws Exception; boolean unlock(); } |
@Slf4j public class ZkLock implements Lock { //ZkLock的節點鏈接 private static final String ZK_PATH = "/test/lock"; private static final String LOCK_PREFIX = ZK_PATH + "/"; private static final long WAIT_TIME = 1000; //Zk客戶端 CuratorFramework client = null; private String locked_short_path = null; private String locked_path = null; private String prior_path = null; final AtomicInteger lockCount = new AtomicInteger(0); private Thread thread; public ZkLock() { ZKclient.instance.init(); if (!ZKclient.instance.isNodeExist(ZK_PATH)) { ZKclient.instance.createNode(ZK_PATH, null); } client = ZKclient.instance.getClient(); } @Override public boolean lock() { synchronized (this) { if (lockCount.get() == 0) { thread = Thread.currentThread(); lockCount.incrementAndGet(); } else { if (!thread.equals(Thread.currentThread())) { return false; } lockCount.incrementAndGet(); return true; } } try { boolean locked = false; locked = tryLock(); if (locked) { return true; } while (!locked) { await(); //獲取等待的子節點列表 List<String> waiters = getWaiters(); if (checkLocked(waiters)) { locked = true; } } return true; } catch (Exception e) { e.printStackTrace(); unlock(); } return false; } @Override public boolean unlock() { if (!thread.equals(Thread.currentThread())) { return false; } int newLockCount = lockCount.decrementAndGet(); if (newLockCount < 0) { throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + locked_path); } if (newLockCount != 0) { return true; } try { if (ZKclient.instance.isNodeExist(locked_path)) { client.delete().forPath(locked_path); } } catch (Exception e) { e.printStackTrace(); return false; } return true; } private void await() throws Exception { if (null == prior_path) { throw new Exception("prior_path error"); } final CountDownLatch latch = new CountDownLatch(1); //訂閱比自己次小順序節點的刪除事件 Watcher w = new Watcher() { @Override public void process(WatchedEvent watchedEvent) { System.out.println("監聽到的變化 watchedEvent = " + watchedEvent); log.info("[WatchedEvent]節點刪除"); latch.countDown(); } }; client.getData().usingWatcher(w).forPath(prior_path); //訂閱比自己次小順序節點的刪除事件 TreeCache treeCache = new TreeCache(client, prior_path); TreeCacheListener l = new TreeCacheListener() { @Override public void childEvent(CuratorFramework client,TreeCacheEvent event) throws Exception { ChildData data = event.getData(); if (data != null) { switch (event.getType()) { case NODE_REMOVED: log.debug("[TreeCache]節點刪除, path={}, data={}", data.getPath(), data.getData()); latch.countDown(); break; default: break; } } } }; treeCache.getListenable().addListener(l); treeCache.start(); latch.await(WAIT_TIME, TimeUnit.SECONDS); } private boolean tryLock() throws Exception { //創建臨時Znode List<String> waiters = getWaiters(); locked_path = ZKclient.instance .createEphemeralSeqNode(LOCK_PREFIX); if (null == locked_path) { throw new Exception("zk error"); } locked_short_path = getShorPath(locked_path); //獲取等待的子節點列表,判斷自己是否第一個 if (checkLocked(waiters)) { return true; } // 判斷自己排第幾個 int index = Collections.binarySearch(waiters, locked_short_path); if (index < 0) { // 網絡抖動,獲取到的子節點列表裏可能已經沒有自己了 throw new Exception("節點沒有找到: " + locked_short_path); } //如果自己沒有獲得鎖,則要監聽前一個節點 prior_path = ZK_PATH + "/" + waiters.get(index - 1); return false; } private String getShorPath(String locked_path) { int index = locked_path.lastIndexOf(ZK_PATH + "/"); if (index >= 0) { index += ZK_PATH.length() + 1; return index <= locked_path.length() ? locked_path.substring(index) : ""; } return null; } private boolean checkLocked(List<String> waiters) { //節點按照編號,升序排列 Collections.sort(waiters); // 如果是第一個,代表自己已經獲得了鎖 if (locked_short_path.equals(waiters.get(0))) { log.info("成功的獲取分佈式鎖,節點爲{}", locked_short_path); return true; } return false; } /** * 從zookeeper中拿到所有等待節點 */ protected List<String> getWaiters() { List<String> children = null; try { children = client.getChildren().forPath(ZK_PATH); } catch (Exception e) { e.printStackTrace(); return null; } return children; } } |
- ZooKeeper分佈式鎖優缺點:
(1)優點:ZooKeeper分佈式鎖(如InterProcessMutex),能有效地解決分佈式問題,不可重入問題,使用起來也較爲簡單。
(2)缺點:ZooKeeper實現的分佈式鎖,性能並不高。Zk中創建和刪除節點只能通過Leader(主)服務器來執行,然後Leader服務器還需要將數據同步到所有的Follower(從)服務器上,這樣頻繁的網絡通信,性能的短板是非常突出。
目前分佈式鎖,比較成熟、主流的方案有兩種:
(1)基於Redis的分佈式鎖。適用於併發量很大、性能要求很高而可靠性問題可以通過其他方案去彌補的場景。
(2)基於ZooKeeper的分佈式鎖。適用於高可靠(高可用),而併發量不是太高的場景。