1. 程式人生 > 其它 >雪花演算法生成分散式ID

雪花演算法生成分散式ID

分散式主鍵ID生成方案

分散式主鍵ID的生成方案有以下幾種:

  • 資料庫自增主鍵

    缺點:

    1. 匯入舊資料時,可能會ID重複,導致匯入失敗
    2. 分散式架構,多個Mysql例項可能會導致ID重複
  • UUID

    缺點:

    1. 佔用空間大
    2. UUID一般是字串儲存,查詢效率低
    3. 沒有排序,無法趨勢遞增
  • 使用Redis生成ID

    缺點:

    1. 依賴Redis高可用
  • 雪花演算法

    缺點:

    1. 依賴伺服器時間,如果時間回撥,將會導致ID重複

雪花演算法原理

雪花演算法是 Twitter 開源的主鍵生成演算法 snowflake

它用64位二進位制表示主鍵,由5部分組成:

  • 最高位:0,表示正數

  • 41 位 :表示時間戳,毫秒為單位,最多表示 2^41 -1 毫秒,約69年

  • 10 位 : 前5位用來表示機房ID,後5位表示伺服器ID,最多表示 2^5 個機房,和 2^10 個伺服器

  • 最後12位:表示序列號,最多表示 2^12-1 = 4096,即每臺伺服器最多支援每毫秒4096次併發生成

雪花演算法的優點:

  1. 生成效率非常高
  2. 佔用空間相對較少,只用 64 位,即 Long 型別,轉換成字串長度最多19
  3. 生成的主鍵趨勢遞增

雪花演算法Java實現

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Random;

public class SnowflakeIdGenerator {

    /**
     * 時間戳標識所佔二進位制位數
     */
    private static final int TIME_STAMP_BIT_LEN = 41;
    /**
     * 機房標識所佔二進位制位數
     */
    private static final int SERVER_ROOM_BIT_LEN = 5;
    /**
     * 伺服器標識所佔二進位制位數
     */
    private static final int SERVER_BIT_LEN = 5;
    /**
     * 每毫秒中序列所佔二進位制位數
     */
    private static final int SEQ_BIT_LEN = 12;

    /**
     * 時間戳標識向左移動的位數(這裡的1標識最高位)
     */
    private static final int TIME_STAMP_LEFT_BIT_LEN = 64 - 1 - TIME_STAMP_BIT_LEN;
    /**
     * 機房標識左移位數
     */
    private static final int SERVER_ROOM_LEFT_BIT_LEN = TIME_STAMP_LEFT_BIT_LEN - SERVER_ROOM_BIT_LEN;
    /**
     * 伺服器標識左移位數
     */
    private static final int SERVER_LEFT_BIT_LEN = SERVER_ROOM_LEFT_BIT_LEN - SERVER_BIT_LEN;

    /**
     * 開始時間戳,此處為 2022年4月9日
     */
    private static final long START_TIME_STAMP = 1649497879948L;
    /**
     * 上次生成ID的時間戳
     */
    private static long LAST_TIME_STAMP = -1L;
    /**
     * 上一次毫秒記憶體序列值
     */
    private static long LAST_SEQ = 0L;

    /**
     * 獲取機房標識(可以手動定義0-31之間的數)
     */
    private static final long SERVER_ROOM_ID = getServerRoomId();
    /**
     * 獲取伺服器標識(可以手動定義0-31之間的數)
     */
    private static final long SERVER_ID = getServerId();

    /**
     * 機房標識最大值 +1
     */
    private static final int SERVER_ROOM_MAX_NUM_1 = ~(-1 << SERVER_ROOM_BIT_LEN) + 1;
    /**
     * 伺服器標識最大值 +1
     */
    private static final int SERVER_MAX_NUM_1 = ~(-1 << SERVER_BIT_LEN) + 1;
    /**
     * 毫秒記憶體列的最大值
     */
    private static final long SEQ_MAX_NUM = ~(-1 << SEQ_BIT_LEN);

    /**
     * 對伺服器地址的雜湊碼取餘作為伺服器標識
     * TODO 根據實際環境修改該方法,該方法不能應用於開發環境,此處僅作為例子
     *
     * @return 伺服器標識
     */
    private static int getServerId() {
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            return (hostAddress.hashCode() & Integer.MAX_VALUE ) % SERVER_MAX_NUM_1;
        } catch (UnknownHostException e) {
            return new Random().nextInt(SERVER_MAX_NUM_1);
        }
    }

    /**
     * 對伺服器名稱的雜湊碼取餘作為機房標識
     * TODO 根據實際環境修改該方法,該方法不能應用於開發環境,此處僅作為例子
     *
     * @return 機房標識
     */
    private static int getServerRoomId() {
        try {
            String hostName = Inet4Address.getLocalHost().getHostName();
            return (hostName.hashCode() & Integer.MAX_VALUE) % SERVER_ROOM_MAX_NUM_1;
        } catch (Exception e) {
            return new Random().nextInt(SERVER_ROOM_MAX_NUM_1);
        }
    }

    /**
     * 一直迴圈直到獲取到下毫秒的時間戳
     *
     * @param lastMillis
     * @return 下一毫秒的時間戳
     */
    private static long nextMillis(long lastMillis) {
        long now = System.currentTimeMillis();
        while (now <= lastMillis) {
            now = System.currentTimeMillis();
        }
        return now;
    }

    /**
     * 生成唯一ID
     * 須加鎖避免併發問題
     *
     * @return 返回唯一ID
     */
    public synchronized static long generateUniqueId() {
        long currentTimeStamp = System.currentTimeMillis();
        // 如果當前時間小於上一次ID生成的時間戳,說明系統時間回退過,此時因丟擲異常
        if (currentTimeStamp < LAST_TIME_STAMP) {
            throw new RuntimeException(String.format("系統時間錯誤! %d 毫秒內拒絕生成雪花ID", START_TIME_STAMP));
        }
        if (currentTimeStamp == LAST_TIME_STAMP) {
            LAST_SEQ = (LAST_SEQ + 1) & SEQ_MAX_NUM;
            if (LAST_SEQ == 0) {
                currentTimeStamp = nextMillis(LAST_TIME_STAMP);
            }
        } else {
            LAST_SEQ = 0;
        }
        // 上次生成ID的時間戳
        LAST_TIME_STAMP = currentTimeStamp;
        return ((currentTimeStamp - START_TIME_STAMP) << TIME_STAMP_LEFT_BIT_LEN | (SERVER_ROOM_ID << SERVER_ROOM_LEFT_BIT_LEN) | (SERVER_ID << SERVER_LEFT_BIT_LEN) | LAST_SEQ);
    }

    /**
     * 主函式測試
     *
     * @param args
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int num = 100;
        for (int i = 0; i < num; i++) {
            System.out.println(generateUniqueId());
        }
        long end = System.currentTimeMillis();

        System.out.println("共生成 " + num + " 個ID,用時 " + (end - start) + " 毫秒");
    }
}