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


 

一、介紹

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

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

1

  1. 1bit,不用,因為二進制中最高位是符號位,1表示負數,0表示正數。生成的id一般都是用整數,所以最高位固定為0。

  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、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId

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

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

SnowFlake可以保證:

  1. 所有生成的id按時間趨勢遞增
  2. 整個分布式系統內不會產生重復id(因為有datacenterId和workerId來做區分)

二、使用建議

1、改進

其實雪花算法就是把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位存儲,時間戳改成秒為單位…

2、總結:

合理的根據自己的實際情況去設計各個唯一條件的組合,雪花算法只是提供了一種相對合理的方式。
雪花算法這種用位來表示狀態的,我們還可以用在其他方面,比如數據庫存儲,可以用更小的空間去表示不同的狀態位
包括各種底層的比如序列化,也是有用到拆解位,充分利用存儲

三、算法實現

/**
 * 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

四、優化

 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


免責聲明!

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



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