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

Java中雪花算法重復(fù)id問題

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

原理解析

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

符號(hào)位 + 時(shí)間戳 + 數(shù)據(jù)中心ID + 機(jī)器ID + 序列號(hào)

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

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

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

為什么會(huì)出現(xiàn)重復(fù)ID?

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

機(jī)器ID重復(fù)

為什么重復(fù):

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

典型現(xiàn)象

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

如何解決:

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

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

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

(3)利用基礎(chǔ)設(shè)施的唯一性:

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

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

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

// 獲取機(jī)器ID
private static long getNodeId() {
    try {
        InetAddress address = findFirstNonLoopbackAddress();
        String ip = address.getHostAddress();
        int hash = ip.hashCode();
        // 確保非負(fù)數(shù)并取模最大節(jié)點(diǎn)ID
        long nodeId = (hash & 0x7FFFFFFF) % (MAX_NODE_ID + 1);
        System.out.println("使用IP地址: " + ip + " 生成機(jī)器ID: " + nodeId);
        return nodeId;
    } catch (Exception e) {
        // 異常時(shí)隨機(jī)生成節(jié)點(diǎn)ID
        long nodeId = new Random().nextInt((int) (MAX_NODE_ID + 1));
        System.out.println("獲取IP失敗,隨機(jī)生成機(jī)器ID: " + nodeId);
        return nodeId;
    }
}
 
// 查找第一個(gè)非環(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é)調(diào)服務(wù):使用分布式協(xié)調(diào)服務(wù)(如ZooKeeper, etcd, Redis, 數(shù)據(jù)庫)來注冊(cè)節(jié)點(diǎn)并分配唯一的機(jī)器 ID。如Leaf-Snowflake改進(jìn)了雪花算法,機(jī)器ID由Zookeeper協(xié)調(diào)分配、百度UID Generator啟動(dòng)時(shí)向DB注冊(cè)節(jié)點(diǎn)分配唯一worker_id。

流程示例:

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

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

缺點(diǎn): 增加了外部依賴和復(fù)雜度。

(5)設(shè)計(jì)機(jī)器ID位數(shù)的考慮:默認(rèn)10位能支持 1024 個(gè)節(jié)點(diǎn),對(duì)大多數(shù)公司規(guī)模通常夠用??梢罁?jù)業(yè)務(wù)規(guī)模靈活調(diào)整:

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

時(shí)鐘回?fù)?/h2>
  • 為什么重復(fù):系統(tǒng)時(shí)間因?yàn)镹TP同步失敗、閏秒調(diào)整、虛擬機(jī)/容器掛起恢復(fù)、人為設(shè)置錯(cuò)誤等原因發(fā)生了向后跳躍,導(dǎo)致雪花算法生成ID時(shí)使用了之前已生成ID的時(shí)間戳部分,進(jìn)而可能產(chǎn)生重復(fù)ID。

  • 如何解決:大部分雪花算法的優(yōu)秀實(shí)現(xiàn)都包含了時(shí)鐘回?fù)軝z測(cè)和處理機(jī)制,如拋出異常、短暫等待、使用備用邏輯。

(1)預(yù)防為主:禁止手動(dòng)時(shí)間修改;NTP通過頻率調(diào)整、分散度控制、時(shí)鐘篩選、步進(jìn)限制等機(jī)制防止時(shí)間回?fù)?,如使用chrony進(jìn)行平滑時(shí)間調(diào)整(stepping → slewing)、配置clock slew而非 jump避免突變。

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

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

(3)等待時(shí)鐘恢復(fù)(適合毫秒級(jí)輕度回?fù)埽喝舭l(fā)現(xiàn)回?fù)?,不立即?bào)錯(cuò),而是阻塞等待 ,直到系統(tǒng)時(shí)間 ≥ lastTimestamp。該方案短暫阻塞,可能影響性能。

// 處理時(shí)鐘回?fù)?
if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    // 回?fù)軙r(shí)間小于1秒,阻塞等待
    if (offset <= MAX_BACKWARD_TIME) {
        currentTimestamp = waitForClockRecovery(lastTimestamp);
    } else {
        // 回?fù)軙r(shí)間超過1秒,拋出異常
        throw new RuntimeException("Clock moved backwards too much: " + offset + "ms");
    }
}
 
private long waitForClockRecovery(long lastTimestamp) {
    long timestamp = System.currentTimeMillis();
    while (timestamp < lastTimestamp) {
        //短暫休眠避免CPU空轉(zhuǎn)
        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)回?fù)苎a(bǔ)償:通過累積所有歷史回?fù)軙r(shí)間,使生成器內(nèi)部時(shí)間永遠(yuǎn)領(lǐng)先于系統(tǒng)時(shí)間,可避免使用Thread.sleep()造成的性能瓶頸。

// 可容忍的最大時(shí)鐘回?fù)埽ê撩耄?
private static final long MAX_BACKWARD_MS = 1000;
 
// 發(fā)生時(shí)鐘回?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");
    }
    // 記錄回?fù)軙r(shí)間用于補(bǔ)償
    clockOffset += backwardMs;
    // 補(bǔ)償當(dāng)前時(shí)間戳
    currentTimestamp = System.currentTimeMillis()+clockOffset;
}

(5)擴(kuò)展位機(jī)制(秒級(jí)以上嚴(yán)重回?fù)埽盒薷难┗ㄋ惴ńY(jié)構(gòu),預(yù)留幾位用于表示“是否處于回?fù)軤顟B(tài)”或“回?fù)艽螖?shù)”。當(dāng)發(fā)生回?fù)軙r(shí),增加“回?fù)馨姹咎?hào)”,即使時(shí)間戳相同,版本不同也能區(qū)分ID。

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

相關(guān)文章

最新評(píng)論

临桂县| 井冈山市| 恩平市| 新津县| 北碚区| 海兴县| 东海县| 吉林市| 乾安县| 西丰县| 龙胜| 卫辉市| 若羌县| 滨州市| 灌云县| 平湖市| 关岭| 龙川县| 呼玛县| 腾冲县| 辽中县| 河池市| 泾阳县| 磐石市| 吴旗县| 枝江市| 江川县| 德江县| 淮安市| 沽源县| 绍兴县| 东源县| 公主岭市| 凭祥市| 保定市| 武陟县| 盱眙县| 宝兴县| 惠东县| 巩义市| 武乡县|