雪花算法snowflake分布式id生成原理詳解,以及對解決時鐘回撥問題幾種方案討論

一、前言


在日趨復雜的分布式系統中,數據量越來越大,數據庫分庫分表是一貫的垂直水平做法,但是需要一個全局唯一ID標識一條數據或者MQ消息,數據庫id自增就顯然不能滿足要求了。因為場景不同,分布式ID需要滿足以下幾個條件:

全局唯一性,不能出現重復的ID。
趨勢遞增,在MySQL InnoDB引擎中使用的是聚集索引,由于多數RDBMS使用B-tree的數據結構來存儲索引數據,在主鍵的選擇上應該盡量使用有序的主鍵保證寫入性能。
單調遞增,保證下一個ID一定大于上一個ID。例如分布式事務版本號、IM增量消息、排序等特殊需求。
信息安全,對于特殊業務,如訂單等,分布式ID生成應該是無規則的,不能從ID上反解析出流量等敏感信息。
市面上對分布式ID生成大致有幾種算法(一些開源項目都是圍著這幾種算法進行實現和優化):

UUID:因為是本地生成,性能極高,但是生成的ID太長,16字節128位,通常需要字符串類型存儲,且無序,所以很多場景不適用,也不適用于作為MySQL數據庫的主鍵和索引(MySql官方建議,主鍵越短越好;對于InnoDB引擎,索引的無序性可能會引起數據位置頻繁變動,嚴重影響性能)。
數據庫自增ID:每次獲取ID都需要DB的IO操作,DB壓力大,性能低。數據庫宕機對外依賴服務就是毀滅性打擊,不過可以部署數據庫集群保證高可用。
數據庫號段算法:對數據庫自增ID的優化,每次獲取一個號段的值。用完之后再去數據庫獲取新的號段,可以大大減輕數據庫的壓力。號段越長,性能越高,同時如果數據庫宕機,號段沒有用完,短時間還可以對外提供服務。(美團的Leaf、滴滴的TinyId)
雪花算法:Twitter開源的snowflake,以時間戳+機器+遞增序列組成,基本趨勢遞增,且性能很高,因為強依賴機器時鐘,所以需要考慮時鐘回撥問題,即機器上的時間可能因為校正出現倒退,導致生成的ID重復。(百度的uid-generator、美團的Leaf)
雪花算法和數據庫號段算法用的最多,本篇主要對雪花算法原理剖析和解決時鐘回撥問題討論。

二、雪花算法snowflake

1、基本定義


snowflake原理其實很簡單,生成一個64bit(long)的全局唯一ID,標準元素以1bit無用符號位+41bit時間戳+10bit機器ID+12bit序列化組成,其中除1bit符號位不可調整外,其他三個標識的bit都可以根據實際情況調整:

41bit-時間可以表示(1L<<41)/(1000L360024*365)=69年的時間。
10bit-機器可以表示1024臺機器。如果對IDC劃分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32臺機器。
12個自增序列號可以表示2^12個ID,理論上snowflake方案的QPS約為409.6w/s。
注:都是從0開始計數。

2、snowflake的優缺點
優點:

毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
可以不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也非常高。
可以根據自身業務特性分配bit位,非常靈活。
缺點:

強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重復或者服務處于不可用狀態。

三、Java代碼實現snowflake

public class SnowflakeIdGenerator {public static final int TOTAL_BITS = 1 << 6;private static final long SIGN_BITS = 1;private static final long TIME_STAMP_BITS = 41L;private static final long DATA_CENTER_ID_BITS = 5L;private static final long WORKER_ID_BITS = 5L;private static final long SEQUENCE_BITS = 12L;/*** 時間向左位移位數 22位*/private static final long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS;/*** IDC向左位移位數 17位*/private static final long DATA_CENTER_ID_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;/*** 機器ID 向左位移位數 12位*/private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;/*** 序列掩碼,用于限定序列最大值為4095*/private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);/*** 最大支持機器節點數0~31,一共32個*/private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);/*** 最大支持數據中心節點數0~31,一共32個*/private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);/*** 最大時間戳 2199023255551*/private static final long MAX_DELTA_TIMESTAMP = -1L ^ (-1L << TIME_STAMP_BITS);/*** Customer epoch*/private final long twepoch;private final long workerId;private final long dataCenterId;private long sequence = 0L;private long lastTimestamp = -1L;/**** @param workerId 機器ID* @param dataCenterId  IDC ID*/public SnowflakeIdGenerator(long workerId, long dataCenterId) {this(workerId, dataCenterId, null);}/**** @param workerId  機器ID* @param dataCenterId IDC ID* @param epochDate 初始化時間起點*/public SnowflakeIdGenerator(long workerId, long dataCenterId, Date epochDate) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException("worker Id can't be greater than "+ MAX_WORKER_ID + " or less than 0");}if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {throw new IllegalArgumentException("datacenter Id can't be greater than {" + MAX_DATA_CENTER_ID + "} or less than 0");}this.workerId = workerId;this.dataCenterId = dataCenterId;if (epochDate != null) {this.twepoch = epochDate.getTime();} else {//2010-10-11this.twepoch = 1286726400000L;}}public long genID() throws Exception {try {return nextId();} catch (Exception e) {throw e;}}public long getLastTimestamp() {return lastTimestamp;}/*** 通過移位解析出sequence,sequence有效位為[0,12]* 所以先向左移64-12,然后再像右移64-12,通過兩次移位就可以把無效位移除了* @param id* @return*/public long getSequence2(long id) {return (id << (TOTAL_BITS - SEQUENCE_BITS)) >>> (TOTAL_BITS - SEQUENCE_BITS);}/*** 通過移位解析出workerId,workerId有效位為[13,17], 左右兩邊都有無效位* 先向左移 41+5+1,移除掉41bit-時間,5bit-IDC、1bit-sign,* 然后右移回去41+5+1+12,從而移除掉12bit-序列號* @param id* @return*/public long getWorkerId2(long id) {return (id << (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);}/*** 通過移位解析出IDC_ID,dataCenterId有效位為[18,23],左邊兩邊都有無效位* 先左移41+1,移除掉41bit-時間和1bit-sign* 然后右移回去41+1+5+12,移除掉右邊的5bit-workerId和12bit-序列號* @param id* @return*/public long getDataCenterId2(long id) {return (id << (TIME_STAMP_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + WORKER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);}/*** 41bit-時間,左邊1bit-sign為0,可以忽略,不用左移,所以只需要右移,并加上起始時間twepoch即可。* @param id* @return*/public long getGenerateDateTime2(long id) {return (id >>> (DATA_CENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS)) + twepoch;}public long getSequence(long id) {return id & ~(-1L << SEQUENCE_BITS);}public long getWorkerId(long id) {return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);}public long getDataCenterId(long id) {return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);}public long getGenerateDateTime(long id) {return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;}private synchronized long nextId() throws Exception {long timestamp = timeGen();// 1、出現時鐘回撥問題,直接拋異常if (timestamp < lastTimestamp) {long refusedTimes = lastTimestamp - timestamp;// 可自定義異常類throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", refusedTimes));}// 2、時間等于lastTimestamp,取當前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {// 3、時間大于lastTimestamp沒有發生回撥, sequence 從0開始this.sequence = 0L;}lastTimestamp = timestamp;return allocate(timestamp - this.twepoch);}private long allocate(long deltaSeconds) {return (deltaSeconds << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;}private long timeGen() {long currentTimestamp = System.currentTimeMillis();// 時間戳超出最大值if (currentTimestamp - twepoch > MAX_DELTA_TIMESTAMP) {throw new UnsupportedOperationException("Timestamp bits is exhausted. Refusing ID generate. Now: " + currentTimestamp);}return currentTimestamp;}private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 測試* @param args*/public static void main(String[] args) throws Exception {SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1,2);long id = snowflakeIdGenerator.genID();System.out.println("ID=" + id + ", lastTimestamp=" + snowflakeIdGenerator.getLastTimestamp());System.out.println("ID二進制:" + Long.toBinaryString(id));System.out.println("解析ID:");System.out.println("Sequence=" + snowflakeIdGenerator.getSequence(id));System.out.println("WorkerId=" + snowflakeIdGenerator.getWorkerId(id));System.out.println("DataCenterId=" + snowflakeIdGenerator.getDataCenterId(id));System.out.println("GenerateDateTime=" + snowflakeIdGenerator.getGenerateDateTime(id));System.out.println("Sequence2=" + snowflakeIdGenerator.getSequence2(id));System.out.println("WorkerId2=" + snowflakeIdGenerator.getWorkerId2(id));System.out.println("DataCenterId2=" + snowflakeIdGenerator.getDataCenterId2(id));System.out.println("GenerateDateTime2=" + snowflakeIdGenerator.getGenerateDateTime2(id));}}

四、時鐘回撥問題和解決方案討論

1、時間戳自增徹底解決時鐘回撥問題

private long sequence = -1L;
private long startTimestamp = 1623947387000L;
private synchronized  long nextId2() {long sequenceTmp = sequence;sequence = (sequence + 1) & SEQUENCE_MASK;// sequence =0 有可能是初始+1=0,也可能是超過了最大值等于0// 所以把 初始+1=0排除掉if (sequence == 0 && sequenceTmp >= 0) {// sequence自增到最大了,時間戳自增1startTimestamp += 1;}// 生成idreturn allocate(startTimestamp - twepoch);
}

2、緩存歷史序列號緩解時鐘回撥問題

// 記錄近2S的毫秒數的sequence的緩存
private int LENGTH = 2000;
// sequence緩存
private long[] sequenceCycle = new long[LENGTH];private synchronized long nextId() throws Exception {long timestamp = timeGen();int index = (int)(timestamp % LENGTH);// 1、出現時鐘回撥問題,獲取歷史序列號自增if (timestamp < lastTimestamp) {long sequence = 0;do {if ((lastTimestamp - timestamp) > LENGTH) {// 可自定義異常、告警等,短暫不能對外提供,故障轉移,將請求轉發到正常機器。throw new UnsupportedOperationException("The timeback range is too large and exceeds 2000ms caches");}long preSequence = sequenceCycle[index];sequence = (preSequence + 1) & SEQUENCE_MASK;if (sequence == 0) {// 如果取出的歷史序列號+1后已經達到超過最大值,// 則重新獲取timestamp,重新拿其他位置的緩存timestamp = tilNextMillis(lastTimestamp);index = (int)(timestamp % LENGTH);} else {// 更新緩存sequenceCycle[index] = this.sequence;            return allocate((timestamp - this.twepoch), sequence);}} while (timestamp < lastTimestamp);// 如果在獲取緩存的過程中timestamp恢復正常了,就走正常流程}// 2、時間等于lastTimestamp,取當前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);index = (int)(timestamp % LENGTH);}} else {// 3、時間大于lastTimestamp沒有發生回撥, sequence 從0開始this.sequence = 0L;}// 緩存sequence + 更新lastTimestampsequenceCycle[index] = this.sequence;lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

3、等待時鐘校正

private synchronized  long nextId3() {long timestamp = timeGen();// 1、出現時鐘回撥問題,如果回撥幅度不大,等待時鐘自己校正if (timestamp < lastTimestamp) {int sleepCntMax = 2;int sleepCnt = 0;do {long sleepTime = lastTimestamp - timestamp;if (sleepCnt > sleepCntMax) {// 可自定義異常類throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}if (sleepTime <= 500) {try {Thread.sleep(sleepTime);} catch (InterruptedException e) {e.printStackTrace();} finally {sleepCnt++;timestamp = tilNextMillis(lastTimestamp);}} else {// 可自定義異常類throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}} while (timestamp < lastTimestamp);}// 2、時間等于lastTimestamp,取當前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {// 3、時間大于lastTimestamp沒有發生回撥, sequence 從0開始this.sequence = 0L;}lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

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

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

相關文章

【PCB設計經驗】去耦電容如何布局?

0805 和 0603 以及更小 封裝的電容用作于對中高頻的去耦,其擺放位置是有要求的: 一、建議盡可能的靠近主控芯片的 電源管腳放置。 二、使用較寬和短的引線連接到電源和地過孔可以采用如下 圖 4–1 中的圖 ( 2 )、( 3)、 ( 4 )任意一種方式,避免使用長線或者較細的…

自動化運維實驗

目錄 一、實驗拓撲 二、實驗目的 三、實驗步驟 實驗思路&#xff1a; 代碼部分&#xff1a; 四、實驗結果&#xff1a; 一、實驗拓撲 二、實驗目的 利用python腳本&#xff0c;在本地&#xff0c;或者虛擬機里實現&#xff0c;設備CRC數量統計&#xff0c;并輸出成表格 三、實驗…

Wed前端第二次作業

一、作業1&#xff1a;完成自己學校的官網&#xff0c;動忘內容直接貼&#xff0c;至少三個不同的頁面1、界面1&#xff08;1&#xff09;相關代碼<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name&quo…

第5節 大模型分布式推理通信優化與硬件協同

前言 在分布式推理中,多設備(如GPU、CPU)之間的數據傳輸(通信)是連接計算的“橋梁”。如果通信效率低下,即使單設備計算能力再強,整體性能也會大打折扣。想象一下:如果工廠之間的物流卡車跑得比生產速度還慢,再多的工廠也無法提高整體產量。 本節將從最基礎的單設備內…

XGBoost 的適用場景以及與 CNN、LSTM 的區別

XGBoost 的核心優勢與適用場景XGBoost 是一種梯度提升決策樹算法&#xff0c;屬于集成學習方法。它在處理結構化/表格化數據方面表現極其出色&#xff0c;是 Kaggle 競賽和工業界廣泛應用的“冠軍”模型。其核心優勢和應用場景包括&#xff1a;1. 結構化/表格化數據數據形式&a…

快速設計簡單嵌入式操作系統(3):動手實操,基于STC8編寫單任務執行程序,感悟MCU指令的執行過程

引言 前面我們陸續學習了操作系統常見的基礎概念&#xff0c;接著簡單了解了一下8051單片機的內存結構和執行順序切換的相關概念。接下來&#xff0c;我們就開始進行實操&#xff0c;基于8051單片機STC8來編寫一個簡單的操作系統&#xff0c;這里我們先實現一個單任務的執行程…

Spring AI Alibaba - 聊天機器人快速上手

本節對應 Github&#xff1a;https://github.com/JCodeNest/JCodeNest-AI-Alibaba/tree/master/spring-ai-alibaba-helloworld 本文將以阿里巴巴的通義大模型為例&#xff0c;通過 Spring AI Alibaba 組件&#xff0c;手把手帶你完成從零到一的構建過程&#xff1a;首先&#…

串口通信學習

不需要校驗位就選8位&#xff0c;需要校驗位就選9位&#xff01;USRTUSART框圖STM32的外設引腳這是USART的基本結構。數據幀&#xff0c;八位是這個公式還是很重要的&#xff01;如果在編輯器里面使用printf打印漢字的話&#xff0c;會出現亂碼的話&#xff0c;前提是你的編碼格…

面試經典150題[001]:合并兩個有序數組(LeetCode 88)

合并兩個有序數組&#xff08;LeetCode 88&#xff09; https://leetcode.cn/problems/merge-sorted-array/?envTypestudy-plan-v2&envIdtop-interview-150 1. 題目背景 你有兩個已經排好序的數組&#xff1a; nums1&#xff1a;前面是有效數字&#xff0c;后面是空位&…

快速安裝達夢8測試庫

計劃&#xff1a;數據庫名實例名PORT_NUMMAL_INST_DW_PORTMAL_HOSTMAL_PORTMAL_DW_PORTDMDWDBINST_1533615101192.168.207.612510135101*****[2025-08-11 15:14:34]***** Last login: Fri Jul 25 17:36:04 2025 from 192.168.88.48 [rootdm01 ~]# ip a 1: lo: <LOOPBACK,UP,…

Hive中優化問題

一、小文件合并優化Hive中的小文件分為Map端的小文件和Reduce端的小文件。(1)、Map端的小文件優化是通過CombineHiveInputFormat操作。相關的參數是&#xff1a;set hive.input.formatorg.apache.hadoop.hive.ql.io.CombineHiveInputFormat;(2)、Reduce端的小文件合并Map端的小…

tlias智能學習輔助系統--Maven高級-繼承

目錄 一、打包方式與應用場景 二、父子工程繼承關系 1. 父工程配置 2. 子工程配置 三、自定義屬性與引用屬性 1. 定義屬性 2. 在 dependencyManagement 中引用 3. 子工程中引用 四、dependencyManagement 與 dependencies 的區別 五、項目結構示例 六、小結 在實際開…

把 AI 押進“小黑屋”——基于 LLM 的隱私對話沙盒設計與落地

標簽&#xff1a;隱私計算、可信執行環境、LLM、沙盒、內存加密、TEE、SGX、Gramine ---- 1. 背景&#xff1a;甲方爸爸一句話&#xff0c;“數據不能出機房” 我們給某三甲醫院做智能問診助手&#xff0c;模型 70 B、知識庫 300 GB。 甲方只給了兩條鐵律&#xff1a; 1. 患者…

Java 大視界 -- Java 大數據在智能教育學習效果評估指標體系構建與精準評估中的應用(394)

Java 大視界 -- Java 大數據在智能教育學習效果評估指標體系構建與精準評估中的應用&#xff08;394&#xff09;引言&#xff1a;正文&#xff1a;一、傳統學習評估的 “數字陷阱”&#xff1a;看不全、說不清、跟不上1.1 評估維度的 “單行道”1.1.1 分數掩蓋的 “學習真相”…

Dubbo 3.x源碼(33)—Dubbo Consumer接收服務調用響應

基于Dubbo 3.1&#xff0c;詳細介紹了Dubbo Consumer接收服務調用響應 此前我們學習了Dubbo Provider處理服務調用請求的流程&#xff0c;現在我們來學習Dubbo Consumer接收服務調用響應流程。 實際上接收請求和接收響應同屬于接收消息&#xff0c;它們的流程的很多步驟是一樣…

棧和隊列:數據結構中的基礎與應用?

棧和隊列&#xff1a;數據結構中的基礎與應用在計算機科學的領域中&#xff0c;數據結構猶如大廈的基石&#xff0c;支撐著各類復雜軟件系統的構建。而棧和隊列作為兩種基礎且重要的數據結構&#xff0c;以其獨特的特性和廣泛的應用&#xff0c;在程序設計的舞臺上扮演著不可或…

服務端配置 CORS解決跨域問題的原理

服務端配置 CORS&#xff08;跨域資源共享&#xff09;的原理本質是 瀏覽器與服務器之間的安全協商機制。其核心在于服務器通過特定的 HTTP 響應頭聲明允許哪些外部源&#xff08;Origin&#xff09;訪問資源&#xff0c;瀏覽器根據這些響應頭決定是否放行跨域請求。以下是詳細…

Unity筆記(五)知識補充——場景切換、退出游戲、鼠標隱藏鎖定、隨機數、委托

寫在前面&#xff1a;寫本系列(自用)的目的是回顧已經學過的知識、記錄新學習的知識或是記錄心得理解&#xff0c;方便自己以后快速復習&#xff0c;減少遺忘。主要是C#代碼部分。十七、場景切換和退出游戲1、場景切換場景切換使用方法&#xff1a; SceneManager.LoadScene()&a…

用 Spring 思維快速上手 DDD——以 Kratos 為例的分層解讀

用 Spring 思維理解 DDD —— 以 Kratos 為參照 ? 在此前的學習工作中&#xff0c;使用的開發框架一直都是 SpringBoot&#xff0c;對 MVC 架構幾乎是肌肉記憶&#xff1a;Controller 接請求&#xff0c;Service 寫業務邏輯&#xff0c;Mapper 操作數據庫&#xff0c;這套套路…

docspace|Linux|使用docker完全離線化部署onlyoffice之docspace文檔協作系統(全網首發)

一、 前言 書接上回&#xff0c;Linux|實用工具|onlyoffice workspace使用docker快速部署&#xff08;離線和定制化部署&#xff09;-CSDN博客&#xff0c;如果是小公司或者比如某個項目組內部使用&#xff0c;那么&#xff0c;使用docspace這個文檔協同系統是非常合適的&…