雪花算法重復id問題

原理解析

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

符號位 + 時間戳 + 數據中心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. 工作節點ID(5位):用于標識數據中心內的具體工作節點(機器、服務進程、Pod 等),允許每個數據中心最多 2^5 =
    32個工作節點。在實際開發中,數據中心(高位)+
    工作節點(低位)經常被視為一個整體10位的機器ID,用于標識集群中的唯一節點(機器/服務實例),最多允許 2^10 =
    1024個唯一節點。
  5. 序列號(12位):用來解決同一節點在同一毫秒內生成多個 ID
    時的沖突問題。每個節點在每毫秒內都可以獨立地從0開始遞增生成序列號,當序列號用完(達到
    4095)后,會強制等待到下一毫秒再繼續生成。對于12位序列號,單節點每毫秒最多生成4096個ID,要達到這個并發量很極端(單節點超過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最大值 31private final long maxWorkerId = -1L ^ (-1L << workerIdBits);// 數據中心ID最大值 31private 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;// 序列號掩碼 4095private final long sequenceMask = -1L ^ (-1L << sequenceBits);// 工作機器IDprivate final long workerId;// 數據中心IDprivate 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;}// 生成下一個IDpublic 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;// 按規則組合生成IDreturn ((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());}}
}    

4.2、為什么會出現重復ID?

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

4.2.1、機器ID重復

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

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

  • 如何解決:

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

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

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

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

  • Kubernetes StatefulSet會為每個 Pod
    分配一個固定且有序的唯一索引(從0開始)。比如名為snowflake-app的StatefulSet有3個Pod:snowflake-app-0,
    snowflake-app-1, snowflake-app-2。應用程序可以讀取 spec.podName(通常是
    HOSTNAME環境變量),解析末尾的數字索引,將這個索引直接用作工作節點ID。若業務需要擴容至超過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地址 (網絡標識):應用程序直接獲取其運行環境(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();// 確保非負數并取模最大節點IDlong nodeId = (hash & 0x7FFFFFFF) % (MAX_NODE_ID + 1);System.out.println("使用IP地址: " + ip + " 生成機器ID: " + nodeId);return nodeId;} catch (Exception e) {// 異常時隨機生成節點IDlong nodeId = new Random().nextInt((int) (MAX_NODE_ID + 1));System.out.println("獲取IP失敗,隨機生成機器ID: " + nodeId);return nodeId;}
}// 查找第一個非環回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("未找到非環回IPv4地址");
}

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

  • 流程示例:
  1. 節點啟動時,連接到協調服務。

  2. 如果節點宕機或與協調服務斷開連接(session超時),協調服務會自動刪除其對應的臨時節點,該機器ID被釋放,可以被新節點申請使用。

  3. 節點將這個唯一的序號作為它的機器ID(或從中計算 datacenter-id 和 worker-id,如序號 % 1024)。序號在服務運行期間保持不變。

  4. 節點讀取自己創建的節點的序號(如 0000000005)。

  5. 協調服務保證創建的有序節點的名稱(包含一個單調遞增的序號)是唯一的。

  6. 節點嘗試在一個預設的路徑下(如 /snowflake/workers)創建一個臨時有序節點。

  • 優點: 無需預配置,自動處理節點加入/離開,ID分配唯一且可靠,支持大規模集群。

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

(5)設計機器ID位數的考慮:默認10位能支持 1024 個節點,對大多數公司規模通常夠用。可依據業務規模靈活調整:

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

4.2.2、時鐘回撥

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

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

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

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

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

(3)等待時鐘恢復(適合毫秒級輕度回撥):若發現回撥,不立即報錯,而是阻塞等待 ,直到系統時間 ≥ 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)回撥補償:通過累積所有歷史回撥時間,使生成器內部時間永遠領先于系統時間,可避免使用Thread.sleep()造成的性能瓶頸。

// 可容忍的最大時鐘回撥(毫秒)
private static final long MAX_BACKWARD_MS = 1000;// 發生時鐘回撥
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)擴展位機制(秒級以上嚴重回撥):修改雪花算法結構,預留幾位用于表示“是否處于回撥狀態”或“回撥次數”。當發生回撥時,增加“回撥版本號”,即使時間戳相同,版本不同也能區分ID。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91907.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91907.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91907.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MQTT 入門教程:三步從 Docker 部署到 Java 客戶端實現

在物聯網&#xff08;IoT&#xff09;與邊緣計算快速發展的今天&#xff0c;設備間的高效通信成為核心需求。MQTT 作為一種輕量級的發布 / 訂閱模式協議&#xff0c;憑借其低帶寬占用、強穩定性和靈活的消息路由能力&#xff0c;已成為物聯網通信的事實標準。無論是智能家居的設…

公網服務器上Nginx或者Openresty如何屏蔽IP直接掃描

0x01 背景云服務器很多時候為了通信需要設置公網訪問&#xff0c;但是網絡當中存在很多的掃描器&#xff0c;無時無刻在掃描&#xff0c;當80,443端口暴露時&#xff0c;成了這些掃描IP的攻擊對象&#xff0c;無時無刻收到威脅。0x02 掃描攻擊方式1.直接通過公網IP地址進行一些…

C語言(長期更新)第8講 函數遞歸

C語言&#xff08;長期更新&#xff09; 第8講:函數遞歸 跟著潼心走&#xff0c;輕松拿捏C語言&#xff0c;困惑通通走&#xff0c;一去不回頭~歡迎開始今天的學習內容&#xff0c;你的支持就是博主最大的動力。 目錄 C語言&#xff08;長期更新&#xff09; 第8講 函數遞歸…

[硬件電路-129]:模擬電路 - 繼電器的工作原理、關鍵指標、常用芯片與管腳定義

一、工作原理繼電器是一種基于電磁感應原理的自動開關裝置&#xff0c;通過控制小電流電路實現大電流電路的通斷。其核心結構包括&#xff1a;電磁鐵&#xff08;線圈鐵芯&#xff09;&#xff1a;通電時產生磁場&#xff0c;吸引銜鐵動作。觸點系統&#xff1a;包含常開觸點&a…

Haproxy調度算法 - 靜態算法介紹與使用

文章目錄一、概述二、socat工具三、static-rr四、firstHAProxy通過固定參數 balance 指明對后端服務器的調度算法&#xff0c;該參數可以配置在listen或backend選項中。HAProxy的調度算法分為靜態和動態調度算法&#xff0c;但是有些算法可以根據參數在靜態和動態算法中相互轉換…

模擬激光相機工作站版本6.0 5.2.32 6.0.44 6.031 5.2.20

模擬激光相機工作站版本6.0 5.2.32 6.0.44 6.031 5.2.20

AWS Blockchain Templates:快速部署企業級區塊鏈網絡的終極解決方案

無需精通底層架構&#xff0c;一鍵搭建Hyperledger Fabric或以太坊網絡&#xff01;AWS Blockchain Templates 可幫助您快速基于不同的區塊鏈框架在 AWS 上創建和部署區塊鏈網絡。區塊鏈是一種分布式數據庫技術&#xff0c;用于維護不斷增長的交易記錄和智能合約集合&#xff0…

Vue 服務端渲染 Nuxt 使用詳解

Nuxt 是基于 Vue 的高層框架&#xff0c;專注于服務器端渲染應用開發。它封裝了繁瑣的配置和通用模式&#xff0c;提供了開箱即用的 SSR 功能&#xff0c;使開發者能夠專注于編寫業務邏輯。 1. Nuxt 的核心特性 SSR 支持&#xff1a;默認支持服務端渲染&#xff0c;提高應用性…

使用ACK Serverless容器化部署大語言模型FastChat

核心概念 阿里云ACK Serverless&#xff1a;是一種基于 Kubernetes 的無服務器容器服務。用戶無需管理底層節點和服務器&#xff0c;即可快速部署容器化應用&#xff0c;并根據實際使用的 CPU 和內存資源按需付費&#xff0c;只專注于應用本身而非基礎設施管理。 FastChat&…

最新Android Studio漢化教程--兼容插件包

[ ] 軟件版本&#xff1a;Android Studio Meerkat Feature Drop | 2024.3.2 Build #AI-243.25659.59.2432.13423653, built on April 30, 2025 Runtime version: 21.0.613368085-b895.109 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Toolkit: sun.awt.windows.WT…

Unity_數據持久化_IXmlSerializable接口

Unity數據持久化 三、XML數據持久化 3.5 IXmlSerializable接口 3.5.1 IXmlSerializable接口基礎概念 什么是IXmlSerializable接口&#xff1a; IXmlSerializable 是.NET框架提供的一個接口&#xff0c;允許類自定義XML序列化和反序列化的過程。當默認的XML序列化行為無法滿足需…

如何快速解決PDF解密新方法?

有時從網絡下載的PDF文檔會帶有加密限制&#xff0c;導致無法編輯、復制或打印。它的體積僅約10MB&#xff0c;無需安裝&#xff0c;解壓即用。遇到受限制的文件時&#xff0c;只需將其拖入界面&#xff0c;選擇是否覆蓋原文件&#xff0c;點擊執行&#xff0c;瞬間完成解密。「…

譯|數據驅動智慧供應鏈的構成要素與關聯思考

數據質量&#xff0c;通過識別關鍵決策和瓶頸構建信息供應鏈。該模型適用于優化庫存管理、自動化物流、預測需求、實現產品全生命周期追溯及應對突發風險。例如&#xff0c;通過AI機器人自動管理倉庫&#xff0c;或利用數字孿生模擬和優化全球采購網絡。 匯總來自三篇文章&…

OS21.【Linux】環境變量

目錄 1.與環境變量有關的實驗 A.對比命令和自制程序的運行 為什么.像ls、pwd這樣的命令運行是不需要加路徑? 執行自制程序而不加路徑的方法,看看PATH環境變量 方法1:將自制程序移動到系統的搜索路徑下 方法2:臨時修改PATH環境變量 B.查看系統中所有環境變量 解釋幾個常…

加密流量論文復現:《Detecting DNS over HTTPS based data exfiltration》(上)

本文將以我個人的理解去閱讀該篇流量加密論文&#xff0c;并在下一篇盡力對其中的實驗部分進行復現。話不多說&#xff0c;先從論文開始著手。 內容介紹 傳統的DNS(Domain Name System)協議是以明文傳輸的。DNS作為互聯網的基礎設施&#xff0c;最初設計時主要考慮的是功能和效…

Apache RocketMQ 中Message (消息)的核心概念

好的&#xff0c;我們來深入理解一下 Apache RocketMQ 中 Message (消息) 這個核心概念。這份文檔詳細闡述了消息的定義、在模型中的位置、內部屬性、約束和使用建議。 你可以將 Message 看作是 RocketMQ 系統中數據傳輸和處理的最小原子單位。它承載了業務數據&#xff0c;并附…

C 語言問題

1. C語言中 union 與 struct 的區別類型structunion內存分配機制編譯器為每個成員?獨立分配內存空間&#xff0c;總內存大小 所有成員大小之和&#xff08;考慮內存對齊&#xff09;所有成員?共享同一段內存空間&#xff0c;總內存大小 ?最大成員的大小?數據存儲特性1. 所…

[ LeetCode優選算法專題一雙指針-----盛最多的水]

1.題目鏈接 LeetCode盛最多的水 2.題目描述 3.題目解析 問題本質分析 "盛最多水的容器" 問題可以抽象為&#xff1a;在坐標軸上有 n 條垂直線段&#xff0c;第 i 條線段的兩個端點分別是 (i, 0) 和 (i, height [i])。找到兩條線段&#xff0c;使得它們與 x 軸共同…

舊筆記本電腦如何安裝飛牛OS

01引言隨著電子產品的更新換代&#xff0c;我們有很多的電子產品已經滿足不了現在的工作需求和日常娛樂了&#xff0c;比如&#xff1a;用了很久厚重筆記本電腦放在現在辦公也是有點吃力了&#xff0c;我們現在換新了舊的還不想放在那里吃灰&#xff0c;怎么辦呢&#xff1f;我…

某金服Java面試終極指南:25題完整解析與場景化方案

涵蓋分布式鎖、緩存、事務、高并發等金融系統核心考點&#xff0c;附解決方案與抗風險設計一、分布式鎖深度解決方案 1. Redis分布式鎖完整實現 // 原子加鎖 防死鎖 String uuid UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue().setIfAbsent(&qu…