雪花算法 Java實現

轉載自:http://www.machengyu.net/tech/2019/12/04/snowflake.html

敘述

snowflake中文的意思是 雪花,雪片,所以翻譯成雪花算法。它最早是twitter內部使用的分佈式環境下的唯一ID生成算法。在2014年開源。開源的版本由scala編寫,大家可以再找個地址找到這版本。

https://github.com/twitter-archive/snowflake/tags

雪花算法產生的背景當然是twitter高併發環境下對唯一ID生成的需求,得益於twitter內部牛逼的技術,雪花算法流傳至今並被廣泛使用。它至少有如下幾個特點:

  • 能滿足高併發分佈式系統環境下ID不重複
  • 基於時間戳,可以保證基本有序遞增(有些業務場景對這個又要求)
  • 不依賴第三方的庫或者中間件
  • 生成效率極高

原理

雪花算法的原理其實非常簡單,我覺得這也是該算法能廣爲流傳的原因之一吧。

算法產生的是一個long型 64 比特位的值,第一位未使用。接下來是41位的毫秒單位的時間戳,我們可以計算下:

2^41/1000*60*60*24*365 = 69

也就是這個時間戳可以使用69年不重複,這個對於大部分系統夠用了。

很多人這裏會搞錯概念,以爲這個時間戳是相對於一個我們業務中指定的時間(一般是系統上線時間),而不是1970年。這裏一定要注意。

10位的數據機器位,所以可以部署在1024個節點。

12位的序列,在毫秒的時間戳內計數。 支持每個節點每毫秒產生4096個ID序號,所以最大可以支持單節點差不多四百萬的併發量,這個妥妥的夠用了。

java實現

雪花算法因爲原理簡單清晰,所以實現的話基本大同小異。下面這個版本跟網上的很多差別也不大。唯一就是我加了一些註釋方便你理解。

public class SnowflakeIdWorker {
    /** 開始時間截 (這個用自己業務系統上線的時間) */
    private final long twepoch = 1575365018000L;

    /** 機器id所佔的位數 */
    private final long workerIdBits = 10L;

    /** 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
    /** 序列在id中佔的位數 */
    private final long sequenceBits = 12L;

    /** 機器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 時間截向左移22位(10+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits;

    /** 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作機器ID(0~1024) */
    private long workerId;

    /** 毫秒內序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的時間截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /**
     * 構造函數
     * @param workerId 工作ID (0~1024)
     */
    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

    // ==============================Methods==========================================
    /**
     * 獲得下一個ID (該方法是線程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一時間生成的,則進行毫秒內序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內序列溢出
            if (sequence == 0) {
                //阻塞到下一個毫秒,獲得新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時間戳改變,毫秒內序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時間截
        lastTimestamp = timestamp;

        //移位並通過或運算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一個毫秒,直到獲得新的時間戳
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當前時間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒爲單位的當前時間
     * @return 當前時間(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

可能有些人對於下面這個生成最大值得方法不太理解,我這裏解釋下,

private final long sequenceMask = -1L ^ (-1L << sequenceBits);

首先我們要知道負數在計算機裏是以補碼的形式表達的,而補碼是負數的絕對值的原碼,再取得反碼,然後再加1得到。

好像有點亂是吧,舉個例子吧。

-1取絕對值是1,1的二進制表示,也就是原碼是:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

然後取反操作,也就是1變0; 0變1,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110

然後加1,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

OK,這就是-1在計算機中的表示了,然後我們來看看(-1L « sequenceBits),這個很簡單,直接左移12個比特位即可,得到:

11111111 11111111 11111111 11111111 11111111 11111111 11110000 00000000

上面兩個異或,得到:

00000000 00000000 00000000 00000000 00000000 00000000 00001111 11111111

也就是4095。

上面第一部分說到雪花算法的性能比較高,接下來我們測試下性能:

public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1);

        long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; System.currentTimeMillis()-start<1000; i++,count=i) {
            idWorker.nextId();
        }
        long end = System.currentTimeMillis()-start;
        System.out.println(end);
        System.out.println(count);
    }

用上面這段代碼,在我自己的評估筆記本上,美妙可以產生400w+的id。效率還是相當高的。

細節

調整比特位分佈

很多公司會根據 snowflake 算法,根據自己的業務做二次改造。舉個例子。你們公司的業務評估不需要運行69年,可能10年就夠了。但是集羣的節點可能會超過1024個,這種情況下,你就可以把時間戳調整成39bit,然後workerid調整爲12比特。同時,workerid也可以拆分下,比如根據業務拆分或者根據機房拆分等。類似如下:

workerid一般如何生成

方案有很多。比如我們公司以前用過通過jvm啓動參數的方式傳過來,應用啓動的時候獲取一個啓動參數,保證每個節點啓動的時候傳入不同的啓動參數即可。啓動參數一般是通過-D選項傳入,示例:

-Dname=value

然後我們在代碼中可以通過

System.getProperty("name");

獲取,或者通過 @value註解也能拿到。

還有問題,現在很多部署都是基於k8s的容器化部署,這種方案往往是基於同一個yaml文件一鍵部署多個容器。所以沒法通過上面的方法每個節點傳入不同的啓動參數。

這個問題可以通過在代碼中根據一些規則計算workerid,比如根據節點的IP地址等。下面給出一個方案:

private static long makeWorkerId() {
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ips = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for (int ip: ips) {
                sums += ip;
            }
            return (sums % 1024);
        } catch (UnknownHostException e) {
            return RandomUtils.nextLong(0, 1024);
        }
    }

這裏其實是獲取了節點的IP地址,然後把ip地址中的每個字節的ascii碼值相加然後對最大值取模。當然這種方法是有可能產生重複的id的。

網上還有一些其它方案,比如取IP的後面10個比特位等。

總之不管用什麼方案,都要儘量保證workerid不重複,否則即便是在併發量不高的情況下,也很容易出現id重複的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章