zookeeper snowflake 實戰



瘋狂創客圈,一個Java 高並發研習社群博客園 總入口

瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高並發實戰

書籍


@

寫在前面

​ 大家好,我是作者尼恩。目前和幾個小伙伴一起,組織了一個高並發的實戰社群【瘋狂創客圈】。正在開始高並發、億級流程的 IM 聊天程序 學習和實戰

​ 前面,已經完成一個高性能的 Java 聊天程序的四件大事:

接下來,需要進入到分布式開發的環節了。 分布式的中間件,瘋狂創客圈的小伙伴們,一致的選擇了zookeeper,不僅僅是由於其在大數據領域,太有名了。更重要的是,很多的著名框架,都使用了zk。

​ **本篇介紹 ZK 的分布式命名服務 ** 中的 節點命名服務和 snowflake 雪花算法。

1.1.1. 集群節點的命名服務

前面講到,在分布式集群中,可能需要部署的大量的機器節點。在節點少的受,可以人工維護。在量大的場景下,手動維護成本高,考慮到自動部署、運維等等問題,節點的命名,最好由系統自動維護。

節點的命名,主要是為節點進行唯一編號。主要的訴求是,不同節點的編號,是絕對的不能重復。一旦編號重復,就會導致有不同的節點碰撞,導致集群異常。

有以下兩個方案,可供生成集群節點編號:

(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);

    }
}

1.1.2. snowflake 的ID算法改造

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)如果當前時間小於記錄的上一個毫秒值,則說明這台機器的時間回撥了,拋出異常。

SnowFlake算法的優點:

(1)生成ID時不依賴於數據庫,完全在內存生成,高性能高可用。

(2)容量大,每秒可生成幾百萬ID。

(3)ID呈趨勢遞增,后續插入數據庫的索引樹的時候,性能較高。

SnowFlake算法的缺點:

(1)依賴於系統時鍾的一致性。如果某台機器的系統時鍾回撥,有可能造成ID沖突,或者ID亂序。

(2)還有,在啟動之前,如果這台機器的系統時間回撥過,那么有可能出現ID重復的危險。

寫在最后

​ 下一篇:基於zk,實現分布式鎖。


瘋狂創客圈 億級流量 高並發IM 實戰 系列

  • Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM