全局惟一iD的生成 雪花算法詳解及其餘用法

 

1、介紹

雪花算法的原始版本是scala版,用於生成分佈式ID(純數字,時間順序),訂單編號等。html

自增ID:對於數據敏感場景不宜使用,且不適合於分佈式場景。
GUID:採用無心義字符串,數據量增大時形成訪問過慢,且不宜排序。java

1git

  1. 1bit,不用,由於二進制中最高位是符號位,1表示負數,0表示正數。生成的id通常都是用整數,因此最高位固定爲0。github

  2. 41bit-時間戳,用來記錄時間戳,毫秒級。
    - 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算法

  3. 10bit-工做機器id,用來記錄工做機器id。
    - 能夠部署在2^{10} = 1024個節點,包括5位datacenterId和5位workerId
    - 5位(bit)能夠表示的最大正整數是2^{5}-1 = 31,便可以用0、一、二、三、....31這32個數字,來表示不一樣的datecenterId或workerIdspring

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

因爲在Java中64bit的整數是long類型,因此在Java中SnowFlake算法生成的id就是long來存儲的。數據庫

SnowFlake能夠保證:apache

  1. 全部生成的id按時間趨勢遞增
  2. 整個分佈式系統內不會產生重複id(由於有datacenterId和workerId來作區分)

2、使用建議

一、改進

其實雪花算法就是把id按位打散,而後再分紅上面這幾塊,用位來表示狀態,這其實就是一種思想。
因此我們實際在用的時候,也沒必要非得按照上面這種分割,只需保證總位數在64位便可安全

若是你的業務不須要69年這麼長,或者須要更長時間
用42位存儲時間戳,(1L << 42) / (1000L * 60 * 60 * 24 * 365) = 139年
用41位存儲時間戳,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
用40位存儲時間戳,(1L << 40) / (1000L * 60 * 60 * 24 * 365) = 34年
用39位存儲時間戳,(1L << 39) / (1000L * 60 * 60 * 24 * 365) = 17年
用38位存儲時間戳,(1L << 38) / (1000L * 60 * 60 * 24 * 365) = 8年
用37位存儲時間戳,(1L << 37) / (1000L * 60 * 60 * 24 * 365) = 4年

若是你的機器沒有那麼1024個這麼多,或者比1024還多
用7位存儲機器id,(1L << 7) = 128
用8位存儲機器id,(1L << 8) = 256
用9位存儲機器id,(1L << 9) = 512
用10位存儲機器id,(1L << 10) = 1024
用11位存儲機器id,(1L << 11) = 2048
用12位存儲機器id,(1L << 12) = 4096
用13位存儲機器id,(1L << 13) = 8192

若是你的業務,每一個機器,每毫秒最多也不會4096個id要生成,或者比這個還多
用8位存儲隨機序列,(1L << 8) = 256
用9位存儲隨機序列,(1L << 9) = 512
用10位存儲隨機序列,(1L << 10) = 1024
用11位存儲隨機序列,(1L << 11) = 2048
用12位存儲隨機序列,(1L << 12) = 4096
用13位存儲隨機序列,(1L << 13) = 8192
用14位存儲隨機序列,(1L << 14) = 16384
用15位存儲隨機序列,(1L << 15) = 32768
注意,隨機序列建議不要太大,通常業務,每毫秒要是能產生這麼多id,建議在機器id上增長位

若是你的業務量很小,好比通常狀況下每毫秒生成不到1個id,此時能夠將隨機序列設置成隨機開始自增
好比從0到48隨機開始自增,算是一種優化建議

若是你有多個業務,也能夠拿出來幾位來表示業務,好比用最後4位,支持16種業務的區分

若是你的業務特別複雜,能夠考慮128位存儲,不過這樣的話,也能夠考慮使用uuid了,但uuid無序,這個有序

若是你的業務很簡單,甚至能夠考慮32位存儲,時間戳改爲秒爲單位…

二、總結:

合理的根據本身的實際狀況去設計各個惟一條件的組合,雪花算法只是提供了一種相對合理的方式。
雪花算法這種用位來表示狀態的,咱們還能夠用在其餘方面,好比數據庫存儲,能夠用更小的空間去表示不一樣的狀態位
包括各類底層的好比序列化,也是有用到拆解位,充分利用存儲

3、算法實現

/**
 * 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 = 1420041600000L;

    /**
     * 機器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;

    //==============================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("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id 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();
    }

    //==============================Test=============================================

    /**
     * 測試
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 3);
        for (int i = 0; i < 50; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);

    }
}
源碼實現

可能的輸出

         長度:60 value:100001101111110101000011111010101010110101001010000000001000
         長度:18 value:607937840337428488
         25

4、優化

 1 import org.apache.commons.lang3.RandomUtils;
 2 import org.apache.commons.lang3.StringUtils;
 3 import org.apache.commons.lang3.SystemUtils;
 4 import org.apache.logging.log4j.Logger;
 5 import org.apache.logging.log4j.LogManager;
 6 import org.springframework.beans.factory.annotation.Value;
 7 import org.springframework.context.annotation.Bean;
 8 import org.springframework.context.annotation.Configuration;
 9 import org.springframework.context.annotation.Primary;
10 import spring.cloud.common.util.id.SnowflakeIdWorker;
11 import java.net.Inet4Address;
12 import java.net.UnknownHostException;
13  
14 /**
15  * 網上的教程通常存在兩個問題:
16  * 1. 機器ID(5位)和數據中心ID(5位)配置沒有解決,分佈式部署的時候會使用相同的配置,任然有ID重複的風險。
17  * 2. 使用的時候須要實例化對象,沒有造成開箱即用的工具類。
18  *
19  * 本文針對上面兩個問題進行解決,筆者的解決方案是,workId使用服務器hostName生成,
20  * dataCenterId使用IP生成,這樣能夠最大限度防止10位機器碼重複,可是因爲兩個ID都不能超過32,
21  * 只能取餘數,仍是不免產生重複,可是實際使用中,hostName和IP的配置通常連續或相近,
22  * 只要不是恰好相隔32位,就不會有問題,何況,hostName和IP同時相隔32的狀況更加是幾乎不可能
23  * 的事,平時作的分佈式部署,通常也不會超過10臺容器。使用上面的方法能夠零配置使用雪花算法,
24  * 雪花算法10位機器碼的設定理論上能夠有1024個節點,生產上使用docker配置通常是一次編譯,
25  * 而後分佈式部署到不一樣容器,不會有不一樣的配置,這裏不知道其餘公司是如何解決的,即便有方法
26  * 使用一套配置,而後運行時根據不一樣容器讀取不一樣的配置,可是給每一個容器編配ID,1024個
27  * (大部分狀況下沒有這麼多),彷佛也不太可能,此問題留待往後解決後再行補充。
28  */
29 @Configuration
30 public class IdWorkerConfiguration {
31     Logger logger = LogManager.getLogger();
32  
33     @Value("${id.work:noWorkId}")
34     private String workId;
35     @Value("${id.dateSource:noDateSource}")
36     private String dateSource;
37     @Bean
38     @Primary
39     public SnowflakeIdWorker idWorker(){
40         return new SnowflakeIdWorker(getWorkFromConfig(),getDateFromConfig());
41     }
42  
43     private Long getWorkFromConfig() {
44         if ("noWorkId".equals(workId)) {
45             return getWorkId();
46         } else {
47             //將workId轉換爲Long
48             return 2L;
49         }
50     }
51  
52     private Long getDateFromConfig() {
53         if ("noDateSource".equals(dateSource)) {
54             return getDataCenterId();
55         } else {
56             //將workId轉換爲Long
57             return 2L;
58         }
59     }
60  
61     private Long getWorkId(){
62         try {
63             String hostAddress = Inet4Address.getLocalHost().getHostAddress();
64             int[] ints = StringUtils.toCodePoints(hostAddress);
65             int sums = 0;
66             for(int b : ints){
67                 sums += b;
68             }
69             return (long)(sums % 32);
70         } catch (UnknownHostException e) {
71             // 若是獲取失敗,則使用隨機數備用
72             return RandomUtils.nextLong(0,31);
73         }
74     }
75  
76     private Long getDataCenterId(){
77         int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
78         int sums = 0;
79         for (int i: ints) {
80             sums += i;
81         }
82         return (long)(sums % 32);
83     }
84  
85 }
交給Ioc容器管理

 

參考文章:

https://blog.csdn.net/java_zhangshuai/article/details/86668974

https://www.cnblogs.com/domi22/p/10629704.html

其餘版本:

https://www.cnblogs.com/Hollson/p/9116218.html

相關文章
相關標籤/搜索