最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java中雪花算法重復id問題

 更新時間:2026年07月22日 09:16:24   作者:Mcband  
本文主要介紹了Java中雪花算法重復id問題解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

原理解析

雪花算法實現簡單、適配性強,無論是電商訂單、日志追蹤還是分布式存儲,都能滿足 “唯一、有序、高效、可擴展” 的核心需求,因此成為分布式ID主流選擇。雪花算法生成的ID是一個64位的整數,由多段不同意義的數字拼接而成,這種分段設計讓每個ID既帶著時間印記,又能規(guī)避多機器沖突,就像身份證通過地址碼、出生日期碼、順序碼等分段信息實現全國唯一標識,既有序又精準。

符號位 + 時間戳 + 數據中心ID + 機器ID + 序列號

  1. 符號位(1位):始終為0(表示正數)。這保證了生成的 ID 是正整數。
  2. 時間戳(41位):雪花算法的核心部分, 記錄生成ID時的毫秒級時間戳(當前時間減去起始時間的差值),該部分保證了ID的大體有序性。41位能表示的時間范圍約為 2^41 毫秒 ≈ 69年。使用一個最近的起始時間(如 2025-01-01
    00:00:00),可以大幅減少時間戳占用的位數。
  3. 數據中心ID(5位):用于標識生成ID的邏輯數據中心,允許最多 2^5 = 32個數據中心。
  4. 工作節(jié)點ID(5位):用于標識數據中心內的具體工作節(jié)點(機器、服務進程、Pod 等),允許每個數據中心最多 2^5 =
    32個工作節(jié)點。在實際開發(fā)中,數據中心(高位)+
    工作節(jié)點(低位)經常被視為一個整體10位的機器ID,用于標識集群中的唯一節(jié)點(機器/服務實例),最多允許 2^10 =
    1024個唯一節(jié)點。
  5. 序列號(12位):用來解決同一節(jié)點在同一毫秒內生成多個 ID
    時的沖突問題。每個節(jié)點在每毫秒內都可以獨立地從0開始遞增生成序列號,當序列號用完(達到
    4095)后,會強制等待到下一毫秒再繼續(xù)生成。對于12位序列號,單節(jié)點每毫秒最多生成4096個ID,要達到這個并發(fā)量很極端(單節(jié)點超過400萬QPS),現實中很難溢出。

以下為java實現的雪花算法代碼示例(未考慮時鐘回撥),起始時間決定了算法能生成ID的有效時長,通常將起始時間設為項目上線日期。

public class SnowflakeIdGenerator {
    // 起始時間戳,這里以2025-07-01 00:00:00為基準
    private final long startTimeStamp = 1751299200000L;
    
    // 機器ID所占位數
    private final long workerIdBits = 5L;
    // 數據中心ID所占位數
    private final long dataCenterIdBits = 5L;
    // 序列號所占位數
    private final long sequenceBits = 12L;
    
    // 機器ID最大值 31
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 數據中心ID最大值 31
    private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
    
    // 機器ID向左移位數
    private final long workerIdShift = sequenceBits;
    // 數據中心ID向左移位數
    private final long dataCenterIdShift = sequenceBits + workerIdBits;
    // 時間戳向左移位數
    private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
    
    // 序列號掩碼 4095
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
    // 工作機器ID
    private final long workerId;
    // 數據中心ID
    private final long dataCenterId;
    // 序列號
    private long sequence = 0L;
    // 上次生成ID的時間戳
    private long lastTimestamp = -1L;
    
    // 構造函數
    public SnowflakeIdGenerator(long workerId, long dataCenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("Worker ID 不能大于 " + maxWorkerId + " 或小于 0");
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException("數據中心 ID 不能大于 " + maxDataCenterId + " 或小于 0");
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
    
    // 生成下一個ID
    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();
        
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 當前毫秒內序列號已用完,等待下一毫秒
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 時間戳改變,重置序列號
            sequence = 0L;
        }
        
        lastTimestamp = currentTimestamp;
        
        // 按規(guī)則組合生成ID
        return ((currentTimestamp - startTimeStamp) << timestampShift) |
                (dataCenterId << dataCenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }
    
    // 等待下一毫秒
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
    
    // 測試示例
    public static void main(String[] args) {
        SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
        for (int i = 0; i < 10; i++) {
            System.out.println(idGenerator.nextId());
        }
    }
}    

為什么會出現重復ID?

雪花算法雖代碼量少、實現簡單,卻并非萬無一失。不少研發(fā)人員常常直接從網上拷貝現成的工具類,或是用大模型生成代碼后直接用于生產環(huán)境 —— 直到某天突然收到用戶反饋:自己賬號的數據出現了錯亂,明明只買了一件衣服,訂單卻顯示多個其他辣眼的商品,還附帶陌生的收貨地址。手忙腳亂一頓排查后,竟發(fā)現數據庫中出現了少量訂單SN重復的異常數據,不由得心生疑惑:雪花算法不是每毫秒能生成4096個不重復編號嗎?訂單服務部署了十幾個節(jié)點,但業(yè)務量真有這么大嗎?到底為什么會出現重復呢?我們一起來一探究竟。

機器ID重復

為什么重復:

多個運行的節(jié)點使用相同的數據中心ID(datacenter-id)和工作節(jié)點ID(worker-id)。即在同一毫秒內,如果多個節(jié)點的機器ID相同、系統(tǒng)時間戳相同,序列號就可能從相同起點開始分配并重疊,導致生成完全相同的ID三元組
(時間戳, 機器ID, 序列號)。

典型現象

多數研發(fā)人員會將數據中心ID和工作節(jié)點ID硬編碼在代碼中,或在配置文件里設置了相同的
datacenter-id與worker-id,這直接導致無論部署多少個節(jié)點,機器ID都完全一致。

如何解決:

核心原則必須確保整個分布式集群中,任何兩個同時工作的節(jié)點,它們的 (數據中心ID, 工作節(jié)點ID) 二元組(或者將二者視為10位合并的“機器ID”)必須是唯一的!

(1)手動配置文件:在啟動服務前,為每個節(jié)點的配置文件(如 application.properties, application.yml, configmap 等)顯式配置一個唯一的 datacenter-id 和 worker-id。該方案簡單直觀,但繁瑣,易出錯(配置沖突),適合小型、靜態(tài)集群,不適用于節(jié)點動態(tài)伸縮的集群。

(2)系統(tǒng)環(huán)境變量:在部署節(jié)點(物理機、虛擬機、容器)時,通過啟動腳本、容器編排系統(tǒng)(如K8s Deployment/StatefulSet 的env)為每個實例設置唯一的 SNOWFLAKE_DATACENTER_ID和SNOWFLAKE_WORKER_ID環(huán)境變量,服務啟動時讀取這些環(huán)境變量。

(3)利用基礎設施的唯一性:

  • Kubernetes StatefulSet會為每個 Pod
    分配一個固定且有序的唯一索引(從0開始)。比如名為snowflake-app的StatefulSet有3個Pod:snowflake-app-0,
    snowflake-app-1, snowflake-app-2。應用程序可以讀取 spec.podName(通常是
    HOSTNAME環(huán)境變量),解析末尾的數字索引,將這個索引直接用作工作節(jié)點ID。若業(yè)務需要擴容至超過worker-id最大閾值(如32個以上Pod),直接使用索引會導致worker-id重復,需結合數據中心ID(datacenter-id)拆分(如用 StatefulSet 名稱哈希作為datacenter-id)。

  • 公有云(如阿里云、華為云、騰訊云ECS)會為每個虛擬機實例分配一個唯一ID,Pod(如Deployment)運行時也有自己的ID。應用程序可以在啟動時通過查詢實例/容器的元數據服務獲取這個唯一ID,然后對這個較長的ID進行哈希并取模,映射到可用的datacenter-id和worker-id范圍內(如總ID%1024,得到 0-1023的一個值)。該方案需要依賴特定平臺的 API/服務。哈希取模存在極小沖突風險,需要設計好映射邏輯。

  • 利用IP地址 (網絡標識):應用程序直接獲取其運行環(huán)境(Pod、容器、虛擬機、物理機)的IP地址,對整個IP地址字符串或二進制表示計算哈希值取模,然后取模,映射為datacenter-id和worker-id。在Kubernetes 中,在Kubernetes中,Pod通常可以通過status.podIP獲得,Deployment Pod重建通常會獲得新IP;虛擬機/物理機IP也可能因維護、遷移或網絡配置變更而改變。該方案同樣存在極小概率ID沖突,且需容忍獲取IP的性能開銷和失敗風險。

// 獲取機器ID
private static long getNodeId() {
    try {
        InetAddress address = findFirstNonLoopbackAddress();
        String ip = address.getHostAddress();
        int hash = ip.hashCode();
        // 確保非負數并取模最大節(jié)點ID
        long nodeId = (hash & 0x7FFFFFFF) % (MAX_NODE_ID + 1);
        System.out.println("使用IP地址: " + ip + " 生成機器ID: " + nodeId);
        return nodeId;
    } catch (Exception e) {
        // 異常時隨機生成節(jié)點ID
        long nodeId = new Random().nextInt((int) (MAX_NODE_ID + 1));
        System.out.println("獲取IP失敗,隨機生成機器ID: " + nodeId);
        return nodeId;
    }
}
 
// 查找第一個非環(huán)回IPv4地址
private static InetAddress findFirstNonLoopbackAddress() throws SocketException {
    Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
    while (interfaces.hasMoreElements()) {
        NetworkInterface iface = interfaces.nextElement();
        if (iface.isLoopback() || iface.isVirtual() || !iface.isUp()) {
            continue;
        }
        Enumeration<InetAddress> addresses = iface.getInetAddresses();
        while (addresses.hasMoreElements()) {
            InetAddress addr = addresses.nextElement();
            if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
                return addr;
            }
        }
    }
    throw new RuntimeException("未找到非環(huán)回IPv4地址");
}

(4)外部協(xié)調服務:使用分布式協(xié)調服務(如ZooKeeper, etcd, Redis, 數據庫)來注冊節(jié)點并分配唯一的機器 ID。如Leaf-Snowflake改進了雪花算法,機器ID由Zookeeper協(xié)調分配、百度UID Generator啟動時向DB注冊節(jié)點分配唯一worker_id。

流程示例:

  • 節(jié)點啟動時,連接到協(xié)調服務。
  • 如果節(jié)點宕機或與協(xié)調服務斷開連接(session超時),協(xié)調服務會自動刪除其對應的臨時節(jié)點,該機器ID被釋放,可以被新節(jié)點申請使用。
  • 節(jié)點將這個唯一的序號作為它的機器ID(或從中計算 datacenter-id 和 worker-id,如序號 % 1024)。序號在服務運行期間保持不變。
  • 節(jié)點讀取自己創(chuàng)建的節(jié)點的序號(如 0000000005)。
  • 協(xié)調服務保證創(chuàng)建的有序節(jié)點的名稱(包含一個單調遞增的序號)是唯一的。
  • 節(jié)點嘗試在一個預設的路徑下(如 /snowflake/workers)創(chuàng)建一個臨時有序節(jié)點。

優(yōu)點: 無需預配置,自動處理節(jié)點加入/離開,ID分配唯一且可靠,支持大規(guī)模集群。

缺點: 增加了外部依賴和復雜度。

(5)設計機器ID位數的考慮:默認10位能支持 1024 個節(jié)點,對大多數公司規(guī)模通常夠用??梢罁I(yè)務規(guī)模靈活調整:

  • 并發(fā)量高但集群規(guī)模不大(節(jié)點少): 可以減少datacenter-id和worker-id 總位數(比如降到8位甚至更少),把節(jié)省出來的位數加到sequence序列號上。這樣每個節(jié)點每毫秒可以生成更多的ID。
  • 集群規(guī)模巨大(超過1024節(jié)點): 需要增加datacenter-id和worker-id總位數(比如設為12位)。這時需要犧牲timestamp或sequence 的位數(如時間戳減到40,序列號減到11位)。犧牲時間戳位數會縮短系統(tǒng)的可用年限;犧牲序列號會降低單節(jié)點/毫秒的最大并發(fā)量。

時鐘回撥

  • 為什么重復:系統(tǒng)時間因為NTP同步失敗、閏秒調整、虛擬機/容器掛起恢復、人為設置錯誤等原因發(fā)生了向后跳躍,導致雪花算法生成ID時使用了之前已生成ID的時間戳部分,進而可能產生重復ID。

  • 如何解決:大部分雪花算法的優(yōu)秀實現都包含了時鐘回撥檢測和處理機制,如拋出異常、短暫等待、使用備用邏輯。

(1)預防為主:禁止手動時間修改;NTP通過頻率調整、分散度控制、時鐘篩選、步進限制等機制防止時間回撥,如使用chrony進行平滑時間調整(stepping → slewing)、配置clock slew而非 jump避免突變。

(2)拋出異常:當檢測到時鐘回撥時,直接拋出異常,停止生成ID,等待人工干預或時間恢復正常。該方案簡單安全,但影響業(yè)務連續(xù)性。

//處理時鐘回撥
if (currentTimestamp < lastTimestamp) {
    throw new ClockBackwardException(
        "Clock moved backwards. Refusing to generate id for " + 
        (lastTimestamp - currentTimestamp) + " milliseconds");
}

(3)等待時鐘恢復(適合毫秒級輕度回撥):若發(fā)現回撥,不立即報錯,而是阻塞等待 ,直到系統(tǒng)時間 ≥ lastTimestamp。該方案短暫阻塞,可能影響性能。

// 處理時鐘回撥
if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    // 回撥時間小于1秒,阻塞等待
    if (offset <= MAX_BACKWARD_TIME) {
        currentTimestamp = waitForClockRecovery(lastTimestamp);
    } else {
        // 回撥時間超過1秒,拋出異常
        throw new RuntimeException("Clock moved backwards too much: " + offset + "ms");
    }
}
 
private long waitForClockRecovery(long lastTimestamp) {
    long timestamp = System.currentTimeMillis();
    while (timestamp < lastTimestamp) {
        //短暫休眠避免CPU空轉
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while waiting for clock recovery", e);
        }
        timestamp = System.currentTimeMillis();
    }
    System.out.println("Clock recovered after " + (timestamp - lastTimestamp) + "ms");
    return timestamp;
}

(4)回撥補償:通過累積所有歷史回撥時間,使生成器內部時間永遠領先于系統(tǒng)時間,可避免使用Thread.sleep()造成的性能瓶頸。

// 可容忍的最大時鐘回撥(毫秒)
private static final long MAX_BACKWARD_MS = 1000;
 
// 發(fā)生時鐘回撥
if (currentTimestamp < lastTimestamp) {
    long backwardMs = lastTimestamp - currentTimestamp;
 
    // 超過容忍閾值則拋出異常
    if (backwardMs > MAX_BACKWARD_MS) {
        throw new IllegalStateException("Clock moved backwards by " + backwardMs + " ms, exceeding maximum allowed value");
    }
    // 記錄回撥時間用于補償
    clockOffset += backwardMs;
    // 補償當前時間戳
    currentTimestamp = System.currentTimeMillis()+clockOffset;
}

(5)擴展位機制(秒級以上嚴重回撥):修改雪花算法結構,預留幾位用于表示“是否處于回撥狀態(tài)”或“回撥次數”。當發(fā)生回撥時,增加“回撥版本號”,即使時間戳相同,版本不同也能區(qū)分ID。

到此這篇關于Java中雪花算法重復id問題的文章就介紹到這了,更多相關Java雪花算法ID重復內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • MyBatis saveBatch 性能調優(yōu)的實現

    MyBatis saveBatch 性能調優(yōu)的實現

    本文主要介紹了MyBatis saveBatch 性能調優(yōu)的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07
  • Spring Boot如何使用httpcomponents實現http請求

    Spring Boot如何使用httpcomponents實現http請求

    這篇文章主要介紹了Spring Boot使用httpcomponents實現http請求的示例代碼,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • 使用SpringMVC訪問Controller接口返回400BadRequest

    使用SpringMVC訪問Controller接口返回400BadRequest

    這篇文章主要介紹了使用SpringMVC訪問Controller接口返回400BadRequest,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • MyBatis插入時獲取自增主鍵方法

    MyBatis插入時獲取自增主鍵方法

    MyBatis 3.2.6插入時候獲取自增主鍵方法有兩種,下面以以MySQL5.5為例通過兩種方法給大家介紹mybatis獲取自增主鍵的方法,一起看看吧
    2016-11-11
  • java?AES加密/解密實現完整代碼(附帶源碼)

    java?AES加密/解密實現完整代碼(附帶源碼)

    這篇文章主要介紹了java?AES加密/解密實現的相關資料,包括AES加密算法的基本原理、Java加密API的使用方法以及項目實現的步驟和代碼示例,需要的朋友可以參考下
    2025-04-04
  • Springboot整合Shiro實現登錄與權限校驗詳細解讀

    Springboot整合Shiro實現登錄與權限校驗詳細解讀

    本文給大家介紹Springboot整合Shiro的基本使用,Apache?Shiro是Java的一個安全框架,Shiro本身無法知道所持有令牌的用戶是否合法,我們將整合Shiro實現登錄與權限的驗證
    2022-04-04
  • Java打包Jar包后使用腳本執(zhí)行

    Java打包Jar包后使用腳本執(zhí)行

    本文詳細講解了Java打包Jar包后使用腳本執(zhí)行的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-12-12
  • Spring?Boot?如何通過ServletRequestHandledEvent事件實現接口請求的性能監(jiān)控

    Spring?Boot?如何通過ServletRequestHandledEvent事件實現接口請求的性能監(jiān)控

    在Spring框架中,監(jiān)控接口請求的性能可以通過ServletRequestHandledEvent事件實現,這篇文章給大家介紹Spring?Boot?如何通過ServletRequestHandledEvent事件實現接口請求的性能監(jiān)控,感興趣的朋友跟隨小編一起看看吧
    2024-08-08
  • 基于Java 數組內存分配的相關問題

    基于Java 數組內存分配的相關問題

    本篇文章是對Java中數組內存分配進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05
  • Java中如何獲取圖片文件格式(后綴)

    Java中如何獲取圖片文件格式(后綴)

    這篇文章主要介紹了Java中如何獲取圖片文件格式(后綴),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06

最新評論

民乐县| 连南| 内丘县| 渑池县| 新民市| 高邑县| 南投市| 徐水县| 育儿| 易门县| 射洪县| 洪洞县| 漳州市| 泗阳县| 丰台区| 十堰市| 柏乡县| 太白县| 齐齐哈尔市| 大竹县| 南开区| 万载县| 界首市| 莫力| 南丰县| 磐石市| 东丽区| 西充县| 牙克石市| 大田县| 黑河市| 江安县| 隆安县| 米泉市| 五寨县| 蒲城县| 溆浦县| 许昌县| 高唐县| 肇庆市| 图片|