1. 程式人生 > 實用技巧 >分散式自增ID生成演算法 - 雪花演算法(SnowFlake)

分散式自增ID生成演算法 - 雪花演算法(SnowFlake)

一、概述

1、SnowFlake演算法生成id的結果是一個64bit大小的整數,它的結構如下圖:

● 1位,不用。二進位制中最高位為1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0

● 41位,用來記錄時間戳(毫秒)。
○ 41位可以表示$2^{41}-1$個數字,
○ 如果只用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0 至 $2^{41}-1$,減1是因為可表示的數值範圍是從0開始算的,而不是1。
○ 也就是說41位可以表示$2^{41}-1$個毫秒的值,轉化成單位年則是$(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年

●10位,用來記錄工作機器id。
○ 可以部署在$2^{10} = 1024$個節點,包括 5位datacenterId 和 5位workerId
○ 5位(bit)可以表示的最大正整數是$2^{5}-1 = 31$,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId

●12位,序列號,用來記錄同毫秒內產生的不同id。
○ 12位(bit)可以表示的最大正整數是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號

由於在Java中64bit的整數是long型別,所以在Java中SnowFlake演算法生成的id就是long來儲存的。

SnowFlake可以保證:
● 所有生成的id按時間趨勢遞增
● 整個分散式系統內不會產生重複id(因為有datacenterId和workerId來做區分)

二、使用

網上的教程一般存在兩個問題:
1. 機器ID(5位)和資料中心ID(5位)配置沒有解決,分散式部署的時候會使用相同的配置,任然有ID重複的風險。
2. 使用的時候需要例項化物件,沒有形成開箱即用的工具類。

本文針對上面兩個問題進行解決,筆者的解決方案是,workId使用伺服器hostName生成,dataCenterId使用IP生成,這樣可以最大限度防止10位機器碼重複,但是由於兩個ID都不能超過32,只能取餘數,還是難免產生重複,但是實際使用中,hostName和IP的配置一般連續或相近,只要不是剛好相隔32位,就不會有問題,況且,hostName和IP同時相隔32的情況更加是幾乎不可能的事,平時做的分散式部署,一般也不會超過10臺容器。

使用上面的方法可以零配置使用雪花演算法,雪花演算法10位機器碼的設定理論上可以有1024個節點,生產上使用docker配置一般是一次編譯,然後分散式部署到不同容器,不會有不同的配置,這裡不知道其他公司是如何解決的,即使有方法使用一套配置,然後執行時根據不同容器讀取不同的配置,但是給每個容器編配ID,1024個(大部分情況下沒有這麼多),似乎也不太可能,此問題留待日後解決後再行補充。

具體生成 workId 和 dataCenterId 的方法如下:

 private static Long getWorkId(){
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){
                sums += b;
            }
            return (long)(sums % 32);
        } catch (UnknownHostException e) {
            // 如果獲取失敗,則使用隨機數備用
            return RandomUtils.nextLong(0,31);
        }
    }
 
    private static Long getDataCenterId(){
        int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
        int sums = 0;
        for (int i: ints) {
            sums += i;
        }
        return (long)(sums % 32);
    }

使用上面的方法需要增加Apache Commons lang3 的依賴,這也是此方法的缺點,但是在實際使用的時候,lang3這個類一般也是要引入的,非常非常好用,提高效率的利器 (注意:這裡的commons-lang3必須是 3.8版本或者更高版本,否則低於這個版本會報沒有toCodePoints(CharSequencestr) 和 getHostName() 方法)。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8</version>
</dependency>

最終的完整程式碼如下:

package com.my.blog.website.utils;
 
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
 
import java.net.Inet4Address;
import java.net.UnknownHostException;
 
/**
 * Twitter_Snowflake<br>
 * SnowFlake的結構如下(每部分用-分開):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位標識,由於long基本型別在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0<br>
 * 41位時間截(毫秒級),注意,41位時間截不是儲存當前時間的時間截,而是儲存時間截的差值(當前時間截 - 開始時間截)
 * 得到的值),這裡的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程式來指定的(如下下面程式IdWorker類的startTime屬性)。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的資料機器位,可以部署在1024個節點,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒內的計數,12位的計數順序號支援每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號<br>
 * 加起來剛好64位,為一個Long型。<br>
 * SnowFlake的優點是,整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞(由資料中心ID和機器ID作區分),並且效率較高,經測試,SnowFlake每秒能夠產生26萬ID左右。
 */
public class SnowflakeIdWorker {
 
    // ==============================Fields===========================================
    /** 開始時間截 (2015-01-01) */
    private final long twepoch = 1489111610226L;
 
    /** 機器id所佔的位數 */
    private final long workerIdBits = 5L;
 
    /** 資料標識id所佔的位數 */
    private final long dataCenterIdBits = 5L;
 
    /** 支援的最大機器id,結果是31 (這個移位演算法可以很快的計算出幾位二進位制數所能表示的最大十進位制數) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 
    /** 支援的最大資料標識id,結果是31 */
    private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
 
    /** 序列在id中佔的位數 */
    private final long sequenceBits = 12L;
 
    /** 機器ID向左移12位 */
    private final long workerIdShift = sequenceBits;
 
    /** 資料標識id向左移17位(12+5) */
    private final long dataCenterIdShift = sequenceBits + workerIdBits;
 
    /** 時間截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
 
    /** 生成序列的掩碼,這裡為4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    /** 工作機器ID(0~31) */
    private long workerId;
 
    /** 資料中心ID(0~31) */
    private long dataCenterId;
 
    /** 毫秒內序列(0~4095) */
    private long sequence = 0L;
 
    /** 上次生成ID的時間截 */
    private long lastTimestamp = -1L;
 
    private static SnowflakeIdWorker idWorker;
 
    static {
        idWorker = new SnowflakeIdWorker(getWorkId(),getDataCenterId());
    }
 
    //==============================Constructors=====================================
    /**
     * 建構函式
     * @param workerId 工作ID (0~31)
     * @param dataCenterId 資料中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long dataCenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
 
    // ==============================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)
                | (dataCenterId << dataCenterIdShift)
                | (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 static Long getWorkId(){
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){
                sums += b;
            }
            return (long)(sums % 32);
        } catch (UnknownHostException e) {
            // 如果獲取失敗,則使用隨機數備用
            return RandomUtils.nextLong(0,31);
        }
    }
 
    private static Long getDataCenterId(){
        int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
        int sums = 0;
        for (int i: ints) {
            sums += i;
        }
        return (long)(sums % 32);
    }
 
 
    /**
     * 靜態工具類
     *
     * @return
     */
    public static synchronized Long generateId(){
        long id = idWorker.nextId();
        return id;
    }
 
    //==============================Test=============================================
    /** 測試 */
    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
        long startTime = System.nanoTime();
        for (int i = 0; i < 50000; i++) {
            long id = SnowflakeIdWorker.generateId();
            System.out.println(id);
        }
        System.out.println((System.nanoTime()-startTime)/1000000+"ms");
    }
}

https://blog.csdn.net/qq_39135287/article/details/88964572