雪花演算法生成分散式ID
阿新 • • 發佈:2022-04-09
分散式主鍵ID生成方案
分散式主鍵ID的生成方案有以下幾種:
-
資料庫自增主鍵
缺點:
- 匯入舊資料時,可能會ID重複,導致匯入失敗
- 分散式架構,多個Mysql例項可能會導致ID重複
-
UUID
缺點:
- 佔用空間大
- UUID一般是字串儲存,查詢效率低
- 沒有排序,無法趨勢遞增
-
使用Redis生成ID
缺點:
- 依賴Redis高可用
-
雪花演算法
缺點:
- 依賴伺服器時間,如果時間回撥,將會導致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次併發生成
雪花演算法的優點:
- 生成效率非常高
- 佔用空間相對較少,只用 64 位,即 Long 型別,轉換成字串長度最多19
- 生成的主鍵趨勢遞增
雪花演算法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) + " 毫秒"); } }