目錄html
瘋狂創客圈 Java 分佈式聊天室【 億級流量】實戰系列之 -25【 博客園 總入口 】java
@面試
你們好,我是做者尼恩。目前和幾個小夥伴一塊兒,組織了一個高併發的實戰社羣【瘋狂創客圈】。正在開始高併發、億級流程的 IM 聊天程序 學習和實戰算法
前面,已經完成一個高性能的 Java 聊天程序的四件大事:數據庫
接下來,須要進入到分佈式開發的環節了。 分佈式的中間件,瘋狂創客圈的小夥伴們,一致的選擇了zookeeper,不單單是因爲其在大數據領域,太有名了。更重要的是,不少的著名框架,都使用了zk。apache
本篇介紹 ZK 的分佈式命名服務 中的 節點命名服務和 snowflake 雪花算法。服務器
前面講到,在分佈式集羣中,可能須要部署的大量的機器節點。在節點少的受,能夠人工維護。在量大的場景下,手動維護成本高,考慮到自動部署、運維等等問題,節點的命名,最好由系統自動維護。併發
節點的命名,主要是爲節點進行惟一編號。主要的訴求是,不一樣節點的編號,是絕對的不能重複。一旦編號重複,就會致使有不一樣的節點碰撞,致使集羣異常。框架
有如下兩個方案,可供生成集羣節點編號:運維
(1)使用數據庫的自增ID特性,用數據表,存儲機器的mac地址或者ip來維護。
(2)使用ZooKeeper持久順序節點的次序特性。來維護節點的編號。
這裏,咱們採用第二種,經過ZooKeeper持久順序節點特性,來配置維護節點的編號NODEID。
集羣節點命名服務的基本流程是:
(1)啓動節點服務,鏈接ZooKeeper, 檢查命名服務根節點根節點是否存在,若是不存在就建立系統根節點。
(2)在根節點下建立一個臨時順序節點,取回順序號作節點的NODEID。如何臨時節點太多,能夠根據須要,刪除臨時節點。
基本的算法,和生成分佈式ID的大部分是一致的,主要的代碼以下:
package com.crazymakercircle.zk.NameService; import com.crazymakercircle.util.ObjectUtil; import com.crazymakercircle.zk.ZKclient; import lombok.Data; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; /** * create by 尼恩 @ 瘋狂創客圈 **/ @Data public class SnowflakeIdWorker { //Zk客戶端 private CuratorFramework client = null; //工做節點的路徑 private String pathPrefix = "/test/IDMaker/worker-"; private String pathRegistered = null; public static SnowflakeIdWorker instance = new SnowflakeIdWorker(); private SnowflakeIdWorker() { instance.client = ZKclient.instance.getClient(); instance.init(); } // 在zookeeper中建立臨時節點並寫入信息 public void init() { // 建立一個 ZNode 節點 // 節點的 payload 爲當前worker 實例 try { byte[] payload = ObjectUtil.Object2JsonBytes(this); pathRegistered = client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(pathPrefix, payload); } catch (Exception e) { e.printStackTrace(); } } public long getId() { String sid=null; if (null == pathRegistered) { throw new RuntimeException("節點註冊失敗"); } int index = pathRegistered.lastIndexOf(pathPrefix); if (index >= 0) { index += pathPrefix.length(); sid= index <= pathRegistered.length() ? pathRegistered.substring(index) : null; } if(null==sid) { throw new RuntimeException("節點ID生成失敗"); } return Long.parseLong(sid); } }
Twitter的snowflake 算法,是一種著名的分佈式服務器用戶ID生成算法。SnowFlake算法所生成的ID 是一個64bit的長整形數字。這個64bit被劃分紅四部分,其中後面三個部分,分別表示時間戳、機器編碼、序號。
(1)第一位
佔用1bit,其值始終是0,沒有實際做用。
(2)時間戳
佔用41bit,精確到毫秒,總共能夠容納約69年的時間。
(3)工做機器id
佔用10bit,最多能夠容納1024個節點。
(4)序列號
佔用12bit,最多能夠累加到4095。這個值在同一毫秒同一節點上從0開始不斷累加。
整體來講,在工做節點達到1024頂配的場景下,SnowFlake算法在同一毫秒內最多能夠生成多少個全局惟一ID呢?這是一個簡單的乘法:
同一毫秒的ID數量 = 1024 X 4096 = 4194304
400多萬個ID,這個數字在絕大多數併發場景下都是夠用的。
snowflake 算法中,第三個部分是工做機器ID,能夠結合上一節的命名方法,並經過Zookeeper管理workId,免去手動頻繁修改集羣節點,去配置機器ID的麻煩。
/** * create by 尼恩 @ 瘋狂創客圈 **/ 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; } }
上面的代碼中,大量的使用到了位運算。
若是對位運算不清楚,估計很難看懂上面的代碼。
這裏須要強調一下,-1 的8位二進制編碼爲 1111 1111,也就是全1。
爲何呢?
由於,8位二進制場景下,-1的原碼是1000 0001,反碼是 1111 1110,補碼是反碼加1。計算後的結果是,-1 的二進制編碼爲全1。16位、32位、64位的-1,二進制的編碼也是全1。
上面用到的二進制位移算法,以及二進制按位或的算法,都比較簡單。若是不懂,能夠去查看java的基礎書籍。
總的來講,以上的代碼,是一個相對比較簡單的snowflake實現版本,關鍵的算法解釋以下:
(1)在單節點上得到下一個ID,使用Synchronized控制併發,而非CAS的方式,是由於CAS不適合併發量很是高的場景。
(2)若是當前毫秒在一臺機器的序列號已經增加到最大值4095,則使用while循環等待直到下一毫秒。
(3)若是當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,拋出異常。
(1)生成ID時不依賴於數據庫,徹底在內存生成,高性能高可用。
(2)容量大,每秒可生成幾百萬ID。
(3)ID呈趨勢遞增,後續插入數據庫的索引樹的時候,性能較高。
(1)依賴於系統時鐘的一致性。若是某臺機器的系統時鐘回撥,有可能形成ID衝突,或者ID亂序。
(2)還有,在啓動以前,若是這臺機器的系統時間回撥過,那麼有可能出現ID重複的危險。
下一篇:基於zk,實現分佈式鎖。
Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
瘋狂創客圈 【 博客園 總入口 】