天天看點

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

一、介紹

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

自增ID:對于資料敏感場景不宜使用,且不适合于分布式場景。

GUID:采用無意義字元串,資料量增大時造成通路過慢,且不宜排序。

1

全局唯一iD的生成 雪花算法詳解及其他用法
  1. 1bit,不用,因為二進制中最高位是符号位,1表示負數,0表示正數。生成的id一般都是用整數,是以最高位固定為0。
  2. 41bit-時間戳,用來記錄時間戳,毫秒級。

    - 41位可以表示

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

    個數字,

    - 如果隻用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0 至

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

    ,減1是因為可表示的數值範圍是從0開始算的,而不是1。

    - 也就是說41位可以表示

    全局唯一iD的生成 雪花算法詳解及其他用法
    個毫秒的值,轉化成機關年則是
    全局唯一iD的生成 雪花算法詳解及其他用法
  3. 10bit-工作機器id,用來記錄工作機器id。

    - 可以部署在

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

    個節點,包括5位datacenterId和5位workerId

    - 5位(bit)可以表示的最大正整數是

    全局唯一iD的生成 雪花算法詳解及其他用法
    ,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId
  4. 12bit-序列号,序列号,用來記錄同毫秒内産生的不同id。

    - 12位(bit)可以表示的最大正整數是

    全局唯一iD的生成 雪花算法詳解及其他用法
    ,即可以用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、總結:

合理的根據自己的實際情況去設計各個唯一條件的組合,雪花算法隻是提供了一種相對合理的方式。

雪花算法這種用位來表示狀态的,我們還可以用在其他方面,比如資料庫存儲,可以用更小的空間去表示不同的狀态位

包括各種底層的比如序列化,也是有用到拆解位,充分利用存儲

三、算法實作

全局唯一iD的生成 雪花算法詳解及其他用法
全局唯一iD的生成 雪花算法詳解及其他用法
/**
 * 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 {
       
</span><span style="color: #008000;">//</span><span style="color: #008000;"> ==============================Fields===========================================</span>
<span style="color: #008000;">/**</span><span style="color: #008000;">
 * 開始時間截 (2015-01-01)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> twepoch = 1420041600000L<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 機器id所占的位數
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> workerIdBits = 5L<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 資料辨別id所占的位數
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> datacenterIdBits = 5L<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 支援的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> maxWorkerId = -1L ^ (-1L &lt;&lt;<span style="color: #000000;"> workerIdBits);

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 支援的最大資料辨別id,結果是31
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> maxDatacenterId = -1L ^ (-1L &lt;&lt;<span style="color: #000000;"> datacenterIdBits);

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 序列在id中占的位數
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> sequenceBits = 12L<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 機器ID向左移12位
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> workerIdShift =<span style="color: #000000;"> sequenceBits;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 資料辨別id向左移17位(12+5)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> datacenterIdShift = sequenceBits +<span style="color: #000000;"> workerIdBits;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 時間截向左移22位(5+5+12)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> timestampLeftShift = sequenceBits + workerIdBits +<span style="color: #000000;"> datacenterIdBits;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 生成序列的掩碼,這裡為4095 (0b111111111111=0xfff=4095)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> sequenceMask = -1L ^ (-1L &lt;&lt;<span style="color: #000000;"> sequenceBits);

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 工作機器ID(0~31)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">long</span><span style="color: #000000;"> workerId;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 資料中心ID(0~31)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">long</span><span style="color: #000000;"> datacenterId;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 毫秒内序列(0~4095)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">long</span> sequence = 0L<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 上次生成ID的時間截
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">long</span> lastTimestamp = -1L<span style="color: #000000;">;

</span><span style="color: #008000;">//</span><span style="color: #008000;">==============================Constructors=====================================</span>

<span style="color: #008000;">/**</span><span style="color: #008000;">
 * 構造函數
 *
 * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> workerId     工作ID (0~31)
 * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> datacenterId 資料中心ID (0~31)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">public</span> SnowflakeIdWorker(<span style="color: #0000ff;">long</span> workerId, <span style="color: #0000ff;">long</span><span style="color: #000000;"> datacenterId) {
    </span><span style="color: #0000ff;">if</span> (workerId &gt; maxWorkerId || workerId &lt; 0<span style="color: #000000;">) {
        </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span> IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0"<span style="color: #000000;">, maxWorkerId));
    }
    </span><span style="color: #0000ff;">if</span> (datacenterId &gt; maxDatacenterId || datacenterId &lt; 0<span style="color: #000000;">) {
        </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span> IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0"<span style="color: #000000;">, maxDatacenterId));
    }
    </span><span style="color: #0000ff;">this</span>.workerId =<span style="color: #000000;"> workerId;
    </span><span style="color: #0000ff;">this</span>.datacenterId =<span style="color: #000000;"> datacenterId;
}

</span><span style="color: #008000;">//</span><span style="color: #008000;"> ==============================Methods==========================================</span>

<span style="color: #008000;">/**</span><span style="color: #008000;">
 * 獲得下一個ID (該方法是線程安全的)
 *
 * </span><span style="color: #808080;">@return</span><span style="color: #008000;"> SnowflakeId
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">long</span><span style="color: #000000;"> nextId() {
    </span><span style="color: #0000ff;">long</span> timestamp =<span style="color: #000000;"> timeGen();

    </span><span style="color: #008000;">//</span><span style="color: #008000;">如果目前時間小于上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當抛出異常</span>
    <span style="color: #0000ff;">if</span> (timestamp &lt;<span style="color: #000000;"> lastTimestamp) {
        </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> RuntimeException(
                String.format(</span>"Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp -<span style="color: #000000;"> timestamp));
    }

    </span><span style="color: #008000;">//</span><span style="color: #008000;">如果是同一時間生成的,則進行毫秒内序列</span>
    <span style="color: #0000ff;">if</span> (lastTimestamp ==<span style="color: #000000;"> timestamp) {
        sequence </span>= (sequence + 1) &amp;<span style="color: #000000;"> sequenceMask;
        </span><span style="color: #008000;">//</span><span style="color: #008000;">毫秒内序列溢出</span>
        <span style="color: #0000ff;">if</span> (sequence == 0<span style="color: #000000;">) {
            </span><span style="color: #008000;">//</span><span style="color: #008000;">阻塞到下一個毫秒,獲得新的時間戳</span>
            timestamp =<span style="color: #000000;"> tilNextMillis(lastTimestamp);
        }
    }
    </span><span style="color: #008000;">//</span><span style="color: #008000;">時間戳改變,毫秒内序列重置</span>
    <span style="color: #0000ff;">else</span><span style="color: #000000;"> {
        sequence </span>= 0L<span style="color: #000000;">;
    }

    </span><span style="color: #008000;">//</span><span style="color: #008000;">上次生成ID的時間截</span>
    lastTimestamp =<span style="color: #000000;"> timestamp;

    </span><span style="color: #008000;">//</span><span style="color: #008000;">移位并通過或運算拼到一起組成64位的ID</span>
    <span style="color: #0000ff;">return</span> ((timestamp - twepoch) &lt;&lt; timestampLeftShift) <span style="color: #008000;">//
           
| (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; }
</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 阻塞到下一個毫秒,直到獲得新的時間戳
 *
 * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> lastTimestamp 上次生成ID的時間截
 * </span><span style="color: #808080;">@return</span><span style="color: #008000;"> 目前時間戳
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">long</span> tilNextMillis(<span style="color: #0000ff;">long</span><span style="color: #000000;"> lastTimestamp) {
    </span><span style="color: #0000ff;">long</span> timestamp =<span style="color: #000000;"> timeGen();
    </span><span style="color: #0000ff;">while</span> (timestamp &lt;=<span style="color: #000000;"> lastTimestamp) {
        timestamp </span>=<span style="color: #000000;"> timeGen();
    }
    </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> timestamp;
}

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 傳回以毫秒為機關的目前時間
 *
 * </span><span style="color: #808080;">@return</span><span style="color: #008000;"> 目前時間(毫秒)
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">long</span><span style="color: #000000;"> timeGen() {
    </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> System.currentTimeMillis();
}

</span><span style="color: #008000;">//</span><span style="color: #008000;">==============================Test=============================================</span>

<span style="color: #008000;">/**</span><span style="color: #008000;">
 * 測試
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
    </span><span style="color: #0000ff;">long</span> start =<span style="color: #000000;"> System.currentTimeMillis();
    SnowflakeIdWorker idWorker </span>= <span style="color: #0000ff;">new</span> SnowflakeIdWorker(1, 3<span style="color: #000000;">);
    </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">int</span> i = 0; i &lt; 50; i++<span style="color: #000000;">) {
        </span><span style="color: #0000ff;">long</span> id =<span style="color: #000000;"> idWorker.nextId();
        System.out.println(Long.toBinaryString(id));
        System.out.println(id);
    }
    </span><span style="color: #0000ff;">long</span> end =<span style="color: #000000;"> System.currentTimeMillis();
    System.out.println(end </span>-<span style="color: #000000;"> start);

}
           
}

源碼實作

可能的輸出

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

四、優化

全局唯一iD的生成 雪花算法詳解及其他用法
全局唯一iD的生成 雪花算法詳解及其他用法
全局唯一iD的生成 雪花算法詳解及其他用法
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 }      
全局唯一iD的生成 雪花算法詳解及其他用法

交給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

原文位址:https://www.cnblogs.com/chaos-li/p/11302820.html