改進雪花算法生成分佈式下全局ID並考慮時鐘回撥

雪花算法-Snowflake

Snowflake,雪花算法是由Twitter開源的分佈式ID生成算法,以劃分命名空間的方式將 64-bit位分割成多個部分,每個部分代表不同的含義。而 Java中64bit的整數是Long類型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 來存儲的。
在這裏插入圖片描述

  • 第1位佔用1bit,其值始終是0,可看做是符號位不使用。
  • 第2位開始的41位是時間戳,41-bit位可表示2^41個數,每個數代表毫秒,那麼雪花算法可用的時間年限是(1L<<41)/(1000L360024*365)=69 年的時間。
  • 中間的10-bit位可表示機器數,即2^10 = 1024臺機器,但是一般情況下我們不會部署這麼臺機器。如果我們對IDC(互聯網數據中心)有需求,還可以將 10-bit 分 5-bit 給 IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32臺機器,具體的劃分可以根據自身需求定義。
  • 最後12-bit位是自增序列,可表示2^12 = 4096個數。這樣的劃分之後相當於在一毫秒一個數據中心的一臺機器上可產生4096個有序的不重複的ID。但是我們 IDC 和機器數肯定不止一個,所以毫秒內能生成的有序ID數是翻倍的。

總體來說,在工作節點達到1024頂配的場景下,SnowFlake算法在同一毫秒內最多可以生成多少個全局唯一ID呢?
這是一個簡單的乘法: 同一毫秒的ID數量 = 1024 X 4096 = 4194304

400多萬個ID,這個數字在絕大多數併發場景下都是夠用的。 snowflake 算法中,第三個部分是工作機器ID,可以通過Zookeeper管理workId,免去手動頻繁修改集羣節點,去配置機器ID的麻煩。

Snowflake 的Twitter官方原版是用Scala寫的,下面是用Java實現的

/**
 * 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左右。
 *
 * @author wsh
 * @version 1.0
 * @since JDK1.8
 * @date 2019/7/31
 */
public class SnowflakeDistributeId {


    // ==============================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 SnowflakeDistributeId(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();
    }
}

測試代碼如下:

public static void main(String[] args) {
    SnowflakeDistributeId idWorker = new SnowflakeDistributeId(0, 0);
    for (int i = 0; i < 1000; i++) {
        long id = idWorker.nextId();
//      System.out.println(Long.toBinaryString(id));
        System.out.println(id);
    }
}

業務場景

上面只是一個將64bit劃分的標準,當然也不一定這麼做,可以根據不同業務的具體場景來劃分,比如下面給出一個業務場景:

  • 服務目前QPS10萬,預計幾年之內會發展到百萬。

  • 當前機器三地部署,上海,北京,深圳都有。

  • 當前機器10臺左右,預計未來會增加至百臺。

這個時候我們根據上面的場景可以再次合理的劃分62bit,QPS幾年之內會發展到百萬,那麼每毫秒就是千級的請求,目前10臺機器那麼每臺機器承擔百級的請求,爲了保證擴展,後面的循環位可以限制到1024,也就是2^10,那麼循環位10位就足夠了。

機器三地部署我們可以用3bit總共8來表示機房位置,當前的機器10臺,爲了保證擴展到百臺那麼可以用7bit 128來表示,時間位依然是41bit,那麼還剩下64-10-3-7-41-1 = 2bit,還剩下2bit可以用來進行擴展。
在這裏插入圖片描述
適用場景: 當我們需要無序不能被猜測的ID,並且需要一定高性能,且需要long型,那麼就可以使用我們雪花算法。比如常見的訂單ID,用雪花算法別人就無法猜測你每天的訂單量是多少。

雪花算法的不足

雪花算法提供了一個很好的設計思想,雪花算法生成的ID是趨勢遞增,不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是非常高的,而且可以根據自身業務特性分配bit位,非常靈活。

但是雪花算法強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重複或者服務會處於不可用狀態。如果恰巧回退前生成過一些ID,而時間回退後,生成的ID就有可能重複。官方對於此並沒有給出解決方案,而是簡單的拋錯處理,這樣會造成在時間被追回之前的這段時間服務不可用。

改進雪花算法防止時鐘回撥

因爲機器的原因會發生時間回撥,我們的雪花算法是強依賴我們的時間的,如果時間發生回撥,有可能會生成重複的ID,在我們上面的nextId中我們用當前時間和上一次的時間進行判斷,如果當前時間小於上一次的時間那麼肯定是發生了回撥,普通的算法會直接拋出異常,這裏我們可以對其進行優化,一般分爲兩個情況:

  • 如果時間回撥時間較短,比如配置5ms以內,那麼可以直接等待一定的時間,讓機器的時間追上來。

  • 如果時間的回撥時間較長,我們不能接受這麼長的阻塞等待,那麼又有兩個策略:


    (1). 直接拒絕,拋出異常,打日誌,通知RD時鐘回滾。
    (2). 利用擴展位,上面我們討論過不同業務場景位數可能用不到那麼多,那麼我們可以把擴展位數利用起來了,比如當這個時間回撥比較長的時候,我們可以不需要等待,直接在擴展位加1。
    2位的擴展位允許我們有3次大的時鐘回撥,一般來說就夠了,如果其超過三次我們還是選擇拋出異常,打日誌。

通過上面的幾種策略可以比較的防護我們的時鐘回撥,防止出現回撥之後大量的異常出現。下面是修改之後的代碼,這裏修改了時鐘回撥的邏輯
代碼如下:

 
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
 
 
/**
 * 分佈式全局ID雪花算法解決方案
 *
 * 防止時鐘回撥
 * 因爲機器的原因會發生時間回撥,我們的雪花算法是強依賴我們的時間的,如果時間發生回撥,
 * 有可能會生成重複的ID,在我們上面的nextId中我們用當前時間和上一次的時間進行判斷,
 * 如果當前時間小於上一次的時間那麼肯定是發生了回撥,
 * 普通的算法會直接拋出異常,這裏我們可以對其進行優化,一般分爲兩個情況:
 * 如果時間回撥時間較短,比如配置5ms以內,那麼可以直接等待一定的時間,讓機器的時間追上來。
 * 如果時間的回撥時間較長,我們不能接受這麼長的阻塞等待,那麼又有兩個策略:
 * 直接拒絕,拋出異常,打日誌,通知RD時鐘回滾。
 * 利用擴展位,上面我們討論過不同業務場景位數可能用不到那麼多,那麼我們可以把擴展位數利用起來了,
 * 比如當這個時間回撥比較長的時候,我們可以不需要等待,直接在擴展位加1。
 * 2位的擴展位允許我們有3次大的時鐘回撥,一般來說就夠了,如果其超過三次我們還是選擇拋出異常,打日誌。
 * 通過上面的幾種策略可以比較的防護我們的時鐘回撥,防止出現回撥之後大量的異常出現。下面是修改之後的代碼,這裏修改了時鐘回撥的邏輯:
 */
public class SnowflakeIdFactory {
 
    private static final Logger log = LoggerFactory.getLogger(SnowflakeIdFactory.class);
 
    /**
     * EPOCH是服務器第一次上線時間點, 設置後不允許修改
     * 2018/9/29日,從此時開始計算,可以用到2089年
     */
    private static long EPOCH = 1538211907857L;
 
    /**
     * 每臺workerId服務器有3個備份workerId, 備份workerId數量越多, 可靠性越高, 但是可部署的sequence ID服務越少
     */
    private static final long BACKUP_COUNT = 3;
 
    /**
     * worker id 的bit數,最多支持8192個節點
     */
    private static final long workerIdBits = 5L;
    /**
     * 數據中心標識位數
     */
    private static final long dataCenterIdBits = 5L;
    /**
     * 序列號,支持單節點最高每毫秒的最大ID數4096
     * 毫秒內自增位
     */
    private static final long sequenceBits = 12L;
 
    /**
     * 機器ID偏左移12位
     */
    private static final long workerIdShift = sequenceBits;
 
    /**
     * 數據中心ID左移17位(12+5)
     */
    private static final long dataCenterIdShift = sequenceBits + workerIdBits;
 
    /**
     * 時間毫秒左移22位(5+5+12)
     */
    private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
    /**
     * sequence掩碼,確保sequnce不會超出上限
     * 最大的序列號,4096
     * -1 的補碼(二進制全1)右移12位, 然後取反
     * 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
     */
    private static final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    //private final static long sequenceMask = ~(-1L << sequenceBits);
    /**
     * 實際的最大workerId的值 結果是31,8091
     * workerId原則上上限爲1024, 但是需要爲每臺sequence服務預留BACKUP_AMOUNT個workerId,
     * (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
     */
    //private static final long maxWorkerId = (1L << workerIdBits) / (BACKUP_COUNT + 1);
 
    //原來代碼 -1 的補碼(二進制全1)右移13位, 然後取反
    private static final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    //private final static long maxWorkerId = ~(-1L << workerIdBits);
 
    /**
     * 支持的最大數據標識id,結果是31
     */
    private static final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
    /**
     * long workerIdBits = 5L;
     * -1L 的二進制: 1111111111111111111111111111111111111111111111111111111111111111
     * -1L<<workerIdBits = -32 ,二進制: 1111111111111111111111111111111111111111111111111111111111100000
     *  workerMask= -1L ^ -32 = 31, 二進制: 11111
     */
    private static long workerMask= -1L ^ (-1L << workerIdBits);
    //進程編碼
    private long processId = 1L;
    private static long processMask=-1L ^ (-1L << dataCenterIdBits);
 
 
    /**
     * 工作機器ID(0~31)
     * snowflake算法給workerId預留了10位,即workId的取值範圍爲[0, 1023],
     * 事實上實際生產環境不大可能需要部署1024個分佈式ID服務,
     * 所以:將workerId取值範圍縮小爲[0, 511],[512, 1023]
     * 這個範圍的workerId當做備用workerId。workId爲0的備用workerId是512,
     * workId爲1的備用workerId是513,以此類推
     */
    private static long workerId;
 
    /**
     * 數據中心ID(0~31)
     */
    private long dataCenterId;
 
    /**
     * 當前毫秒生成的序列
     */
    private long sequence = 0L;
 
    /**
     * 上次生成ID的時間戳
     */
    private long lastTimestamp = -1L;
 
    private long extension = 0L;
    private long maxExtension =  0L;
 
    /**
     * 保留workerId和lastTimestamp, 以及備用workerId和其對應的lastTimestamp
     */
    private static Map<Long, Long> workerIdLastTimeMap = new ConcurrentHashMap<>();
 
    /**
     * 最大容忍時間, 單位毫秒, 即如果時鐘只是回撥了該變量指定的時間, 那麼等待相應的時間即可;
     * 考慮到sequence服務的高性能, 這個值不易過大
     */
    private static final long MAX_BACKWARD_MS = 3;
    private static SnowflakeIdFactory idWorker;
 
    static {
        idWorker = new SnowflakeIdFactory();
    }
 
 
 
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2018, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        // EPOCH是服務器第一次上線時間點, 設置後不允許修改
        EPOCH = calendar.getTimeInMillis();
 
        // 初始化workerId和其所有備份workerId與lastTimestamp
        // 假設workerId爲0且BACKUP_AMOUNT爲4, 那麼map的值爲: {0:0L, 256:0L, 512:0L, 768:0L}
        // 假設workerId爲2且BACKUP_AMOUNT爲4, 那麼map的值爲: {2:0L, 258:0L, 514:0L, 770:0L}
       /* for (int i = 0; i<= BACKUP_COUNT; i++){
            workerIdLastTimeMap.put(workerId + (i * maxWorkerId), 0L);
        }*/
    }
 
    //成員類,IdGenUtils的實例對象的保存域
    private static class SnowflakeIdGenHolder {
        private static final SnowflakeIdFactory instance = new SnowflakeIdFactory();
    }
    //外部調用獲取IdGenUtils的實例對象,確保不可變
    public static SnowflakeIdFactory getInstance(){
        return SnowflakeIdGenHolder.instance;
    }
 
    /**
     * 靜態工具類
     *
     * @return
     */
    public static Long generateId(){
        long id = idWorker.nextId();
        return id;
    }
 
    //初始化構造,無參構造有參函數,默認節點都是0
    public SnowflakeIdFactory(){
        //this(0L, 0L);
        this.dataCenterId = getDataCenterId(maxDataCenterId);
        //獲取機器編碼
        this.workerId = getWorkerId(dataCenterId, maxWorkerId);
    }
 
    /**
     * 構造函數
     * @param workerId 工作ID (0~31)
     * @param dataCenterId 數據中心ID (0~31)
     */
    public SnowflakeIdFactory(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;
    }
 
   /**
     * 獲取帶自定義前綴的全局唯一編碼
     */
    public String getStrCodingByPrefix(String prefix){
        Long ele = this.nextId();
        return prefix + ele.toString();
    }
 
    /**
     * 獲得下一個ID (該方法是線程安全的)
     * 在單節點上獲得下一個ID,使用Synchronized控制併發,而非CAS的方式,
     * 是因爲CAS不適合併發量非常高的場景。
     *
     * 考慮時鐘回撥
     * 缺陷: 如果連續兩次時鐘回撥, 可能還是會有問題, 但是這種概率極低極低
     * @return
     */
    public synchronized long nextId() {
        long currentTimestamp = timeGen();
        // 當發生時鐘回撥時
        if (currentTimestamp < lastTimestamp){
            // 如果時鐘回撥在可接受範圍內, 等待即可
            long offset = lastTimestamp - currentTimestamp;
            if ( offset <= MAX_BACKWARD_MS){
                try {
                    //睡(lastTimestamp - currentTimestamp)ms讓其追上
                    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(offset));
                    //時間偏差大小小於5ms,則等待兩倍時間
                    //wait(offset << 1);
                    //Thread.sleep(waitTimestamp);
 
                    currentTimestamp = timeGen();
                    //如果時間還小於當前時間,那麼利用擴展字段加1
                    //或者是採用拋異常並上報
                    if (currentTimestamp < lastTimestamp) {
                        //擴展字段
                        //extension += 1;
                        //if (extension > maxExtension) {
                            //服務器時鐘被調整了,ID生成器停止服務.
                            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - currentTimestamp));
                        //}
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }else {
                //擴展字段
                /*extension += 1;
                if (extension > maxExtension) {
                    //服務器時鐘被調整了,ID生成器停止服務.
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - currentTimestamp));
                }*/
                tryGenerateKeyOnBackup(currentTimestamp);
            }
        }
        //對時鐘回撥簡單處理
       /* if (currentTimestamp < lastTimestamp) {
            //服務器時鐘被調整了,ID生成器停止服務.
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - currentTimestamp));
        }*/
 
        // 如果和最後一次請求處於同一毫秒, 那麼sequence+1
        if (lastTimestamp == currentTimestamp) {
            // 如果當前生成id的時間還是上次的時間,那麼對sequence序列號進行+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                //自旋等待到下一毫秒
                currentTimestamp = waitUntilNextTime(lastTimestamp);
            }
            //判斷是否溢出,也就是每毫秒內超過4095,當爲4096時,與sequenceMask相與,sequence就等於0
            /*if (sequence == sequenceMask) {
                // 當前毫秒生成的序列數已經大於最大值,那麼阻塞到下一個毫秒再獲取新的時間戳
                currentTimestamp = this.waitUntilNextTime(lastTimestamp);
            }*/
 
        } else {
            // 如果是一個更近的時間戳, 那麼sequence歸零
            sequence = 0L;
        }
        // 更新上次生成id的時間戳
        lastTimestamp = currentTimestamp;
 
        // 更新map中保存的workerId對應的lastTimestamp
        workerIdLastTimeMap.put(this.workerId, lastTimestamp);
 
        if (log.isDebugEnabled()) {
            log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTimestamp)), workerId, sequence);
        }
 
        // 進行移位操作生成int64的唯一ID
        //時間戳右移動23位
        long timestamp = (currentTimestamp - EPOCH) << timestampLeftShift;
 
        //workerId 右移動10位
        long workerId = this.workerId << workerIdShift;
 
        //dataCenterId 右移動(sequenceBits + workerIdBits = 17位)
        long dataCenterId = this.dataCenterId << dataCenterIdShift;
        return timestamp | dataCenterId | workerId | sequence;
    }
 
    /**
     * 嘗試在workerId的備份workerId上生成
     * 核心優化代碼在方法tryGenerateKeyOnBackup()中,BACKUP_COUNT即備份workerId數越多,
     * sequence服務避免時鐘回撥影響的能力越強,但是可部署的sequence服務越少,
     * 設置BACKUP_COUNT爲3,最多可以部署1024/(3+1)即256個sequence服務,完全夠用,
     * 抗時鐘回撥影響的能力也得到非常大的保障。
     * @param currentMillis 當前時間
     */
    private long tryGenerateKeyOnBackup(long currentMillis){
        // 遍歷所有workerId(包括備用workerId, 查看哪些workerId可用)
        for (Map.Entry<Long, Long> entry:workerIdLastTimeMap.entrySet()){
            this.workerId = entry.getKey();
            // 取得備用workerId的lastTime
            Long tempLastTime = entry.getValue();
            lastTimestamp = tempLastTime==null?0L:tempLastTime;
 
            // 如果找到了合適的workerId
            if (lastTimestamp<=currentMillis){
                return lastTimestamp;
            }
        }
 
        // 如果所有workerId以及備用workerId都處於時鐘回撥, 那麼拋出異常
        throw new IllegalStateException("Clock is moving backwards, current time is "
                +currentMillis+" milliseconds, workerId map = " + workerIdLastTimeMap);
    }
 
    /**
     * 阻塞到下一個毫秒,直到獲得新的時間戳
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當前時間戳
     */
    protected long waitUntilNextTime(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
 
    protected long timeGen() {
        return System.currentTimeMillis();
    }
 
    /**
     *  獲取WorkerId
     * @param dataCenterId
     * @param maxWorkerId
     * @return
     */
    protected static long getWorkerId(long dataCenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(dataCenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
           // GET jvmPid
           mpid.append(name.split("@")[0]);
        }
       // MAC + PID 的 hashcode 獲取16個低位
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }
 
    /**
     * 獲取機器編碼 用來做數據ID
     * 數據標識id部分 通常不建議採用下面的MAC地址方式,
     * 因爲用戶通過破解很容易拿到MAC進行破壞
     */
    protected static long getDataCenterId(long tempMaxDataCenterId) {
        if (tempMaxDataCenterId < 0L  || tempMaxDataCenterId > maxDataCenterId) {
            tempMaxDataCenterId = maxDataCenterId;
        }
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (tempMaxDataCenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }
 
 
    public static void testProductIdByMoreThread(int dataCenterId, int workerId, int n) throws InterruptedException {
        List<Thread> tlist = new ArrayList<>();
        Set<Long> setAll = new HashSet<>();
        CountDownLatch cdLatch = new CountDownLatch(10);
        long start = System.currentTimeMillis();
        int threadNo = dataCenterId;
        Map<String,SnowflakeIdFactory> idFactories = new HashMap<>();
        for(int i=0;i<10;i++){
            //用線程名稱做map key.
            idFactories.put("snowflake"+i,new SnowflakeIdFactory(workerId, threadNo++));
        }
        for(int i=0;i<10;i++){
            Thread temp =new Thread(new Runnable() {
                @Override
                public void run() {
                    Set<Long> setId = new HashSet<>();
                    SnowflakeIdFactory idWorker = idFactories.get(Thread.currentThread().getName());
                    for(int j=0;j<n;j++){
                        setId.add(idWorker.nextId());
                    }
                    synchronized (setAll){
                        setAll.addAll(setId);
                        log.info("{}生產了{}個id,併成功加入到setAll中.",Thread.currentThread().getName(),n);
                    }
                    cdLatch.countDown();
                }
            },"snowflake"+i);
            tlist.add(temp);
        }
        for(int j=0;j<10;j++){
            tlist.get(j).start();
        }
        cdLatch.await();
 
        long end1 = System.currentTimeMillis() - start;
 
        log.info("共耗時:{}毫秒,預期應該生產{}個id, 實際合併總計生成ID個數:{}",end1,10*n,setAll.size());
 
    }
 
    public static void testProductId(int dataCenterId, int workerId, int n){
        SnowflakeIdFactory idWorker = new SnowflakeIdFactory(workerId, dataCenterId);
        SnowflakeIdFactory idWorker2 = new SnowflakeIdFactory(workerId+1, dataCenterId);
        Set<Long> setOne = new HashSet<>();
        Set<Long> setTow = new HashSet<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            setOne.add(idWorker.nextId());//加入set
        }
        long end1 = System.currentTimeMillis() - start;
        log.info("第一批ID預計生成{}個,實際生成{}個<<<<*>>>>共耗時:{}",n,setOne.size(),end1);
 
        for (int i = 0; i < n; i++) {
            setTow.add(idWorker2.nextId());//加入set
        }
        long end2 = System.currentTimeMillis() - start;
        log.info("第二批ID預計生成{}個,實際生成{}個<<<<*>>>>共耗時:{}",n,setTow.size(),end2);
 
        setOne.addAll(setTow);
        log.info("合併總計生成ID個數:{}",setOne.size());
 
    }
 
    public static void testPerSecondProductIdNums(){
        SnowflakeIdFactory idWorker = new SnowflakeIdFactory(1, 2);
        long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; System.currentTimeMillis()-start<1000; i++,count=i) {
            /**  測試方法一: 此用法純粹的生產ID,每秒生產ID個數爲300w+ */
            idWorker.nextId();
            /**  測試方法二: 在log中打印,同時獲取ID,此用法生產ID的能力受限於log.error()的吞吐能力.
             * 每秒徘徊在10萬左右. */
            //log.error("{}",idWorker.nextId());
        }
        long end = System.currentTimeMillis()-start;
        System.out.println(end);
        System.out.println(count);
    }
 
    public static void main(String[] args) {
        /** case1: 測試每秒生產id個數?
         *   結論: 每秒生產id個數300w+ */
        testPerSecondProductIdNums();
 
        /** case2: 單線程-測試多個生產者同時生產N個id,驗證id是否有重複?
         *   結論: 驗證通過,沒有重複. */
        //testProductId(1,2,10000);//驗證通過!
        //testProductId(1,2,20000);//驗證通過!
 
        /** case3: 多線程-測試多個生產者同時生產N個id, 全部id在全局範圍內是否會重複?
         *   結論: 驗證通過,沒有重複. */
       /* try {
            testProductIdByMoreThread(1,2,100000);//單機測試此場景,性能損失至少折半!
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
 
    }
}

參考

1. 使用雪花算法爲分佈式下全局ID、訂單號等簡單解決方案考慮到時鐘回撥
2. 雪花算法(改進版)-Snowflake

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