分布式ID生成之雪花算法詳解

分布式ID生成之雪花算法

分布式ID生成方案

唯一ID可以標(biāo)識(shí)數(shù)據(jù)的唯一性颂暇,在分布式系統(tǒng)中生成唯一ID的方案有很多搪柑,常見的方式大概有以下幾種方式:

UUID 隨機(jī)數(shù)

數(shù)據(jù)庫特性

Redis 生成 ID

snowflake 雪花算法


關(guān)于雪花算法

有這樣一種說法,自然界中并不存在兩片完全一樣的雪花(世界上沒有兩片相同的樹葉)并徘,每一片雪花都擁有自己漂亮獨(dú)特的形狀且是獨(dú)一無二。

雪花算法也表示生成的 ID 如雪花般獨(dú)一無二,SnowFlake 算法昼榛,是 Twitter 開源的分布式 ID 生成算法。

雪花算法概述

雪花算法生成的 ID 是純數(shù)字且具有時(shí)間順序的剔难。其原始版本是 scala 版胆屿,后面出現(xiàn)了許多其他語言的版本如Java、C++等偶宫。

組成結(jié)構(gòu)

雪花算法-組成結(jié)構(gòu)

大致是由:首位無效符非迹、時(shí)間戳差值,機(jī)器(進(jìn)程)編碼纯趋,序列號(hào)四部分組成憎兽。

1 bit:首位無效,為啥呢吵冒?

因?yàn)槎M(jìn)制里第一個(gè) bit 為如果是 1唇兑,那么都是負(fù)數(shù),但是我們生成的 id 都是正數(shù)桦锄,所以第一個(gè) bit 統(tǒng)一都是 0扎附。

41 bit:表示的是時(shí)間戳,單位是毫秒

41 bit 可以表示的數(shù)字多達(dá) 2^41 - 1结耀,也就是可以標(biāo)識(shí) 2 ^ 41 - 1 個(gè)毫秒值留夜,換算成年就是表示 69 年的時(shí)間。

10 bit:記錄工作機(jī)器 id图甜,代表的是這個(gè)服務(wù)最多可以部署在 2^10 臺(tái)機(jī)器上碍粥,也就是 1024 臺(tái)機(jī)器

10 bit 里 5 個(gè) bit 代表機(jī)房 id,5 個(gè) bit 代表機(jī)器 id黑毅。意思就是最多代表 2 ^ 5 個(gè)機(jī)房(32 個(gè)機(jī)房)嚼摩,每個(gè)機(jī)房里可以代表 2 ^ 5 個(gè)機(jī)器(32 臺(tái)機(jī)器),也可以根據(jù)自己公司的實(shí)際情況確定。

12 bit:這個(gè)是用來記錄同一個(gè)毫秒內(nèi)產(chǎn)生的不同 id

12 bit 可以代表的最大正整數(shù)是 2 ^ 12 - 1 = 4096枕面,也就是說可以用這個(gè) 12 bit 代表的數(shù)字來區(qū)分同一個(gè)毫秒內(nèi)的 4096 個(gè)不同的 id愿卒。

雪花算法優(yōu)點(diǎn)

自增、有序潮秘、適合分布式場(chǎng)景琼开,生成時(shí)不依賴于數(shù)據(jù)庫,完全在內(nèi)存中生成枕荞,每秒能生成數(shù)百萬的自增 ID柜候,存入數(shù)據(jù)庫中,索引效率高躏精。

時(shí)間位:可以根據(jù)時(shí)間進(jìn)行排序渣刷,有助于提高查詢速度。

機(jī)器 ID 位:適用于分布式環(huán)境下對(duì)多節(jié)點(diǎn)的各個(gè)節(jié)點(diǎn)進(jìn)行標(biāo)識(shí)矗烛,可以具體根據(jù)節(jié)點(diǎn)數(shù)和部署情況設(shè)計(jì)劃分機(jī)器位 10 位長(zhǎng)度飞主,如劃分5位表示進(jìn)程位等。

序列號(hào)位:是一系列的自增id高诺,可以支持同一節(jié)點(diǎn)同一毫秒生成多個(gè) ID 序號(hào),12 位的計(jì)數(shù)序列號(hào)支持每個(gè)節(jié)點(diǎn)每毫秒產(chǎn)生 4096 個(gè) ID 序號(hào)

snowflake 算法可以根據(jù)項(xiàng)目情況以及自身需要進(jìn)行一定的修改碾篡。

基于 Snowflake 算法的其他開源方案 百度 uid-generator 虱而、滴滴 Tinyid 、美團(tuán)leaf

Java實(shí)現(xiàn)雪花雪花系統(tǒng)

使用Java語言實(shí)現(xiàn)雪花算法的ID生成器开泽,可以參考以下代碼牡拇。這個(gè)實(shí)現(xiàn)同樣遵循了雪花算法的基本結(jié)構(gòu),包括1位符號(hào)位穆律、41位時(shí)間戳惠呼、10位機(jī)器標(biāo)識(shí)(5位數(shù)據(jù)中心ID和5位工作機(jī)器ID)以及12位序列號(hào)。我們將這些位數(shù)放在了配置文件中峦耘,家人們可以根據(jù)實(shí)際情況進(jìn)行調(diào)整剔蹋。在這個(gè)代碼中,我們提供了單id生成接口和批量id生成接口辅髓。代碼如下:

配置信息 application.yml

server:

? port: 8000

snowflake:

? #數(shù)據(jù)中心id位數(shù)

? datacenterBits: 5

? # 機(jī)器id位數(shù)

? workerBits: 5

? # 序列id所占位數(shù)

? sequenceBits: 12

? # 數(shù)據(jù)中心id,范圍0-2^5-1

? datacenterId: 1

? # 機(jī)器id,范圍0-2^5-1

? workerId: 1

? # 時(shí)間戳起始點(diǎn)(2024-01-01 00::00:00 的毫秒數(shù))

? twepoch: 1704038400000

? #單次批量生成id的最大數(shù)量? 默認(rèn)10萬

? maxBatchCount: 100000

SnowflakeProperties

import lombok.Data;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

@Component

@ConfigurationProperties(prefix ="snowflake")

@Data

public class SnowflakeProperties {

? ? //數(shù)據(jù)中心id

? ? private Long datacenterId;

? ? //數(shù)據(jù)中心id位數(shù)

? ? private Long datacenterBits;

? ? //機(jī)器id

? ? private Long workerId;

? ? //機(jī)器id位數(shù)

? ? private Long workerBits;

? ? //序列id所占位數(shù)

? ? private Long sequenceBits;

? ? // 時(shí)間戳起始點(diǎn)(毫秒)

? ? private? Long twepoch;

? ? //單次批量生成id的最大數(shù)量

? ? private Integer maxBatchCount;

}

SnowflakeIdGenerator

package cn.xj.snowflake.generator;

import cn.xj.snowflake.config.SnowflakeProperties;

import org.springframework.stereotype.Component;

import java.util.ArrayList;

import java.util.List;

@Component

public class SnowflakeIdGenerator {

? ? //數(shù)據(jù)中心id

? ? private final long datacenterId;

? ? //數(shù)據(jù)中心id位數(shù)

? ? private final long datacenterBits;

? ? //機(jī)器id

? ? private final long workerId;

? ? //機(jī)器id位數(shù)

? ? private final long workerBits;

? ? //序列id所占位數(shù)

? ? private final? long sequenceBits;

? ? // 時(shí)間戳起始點(diǎn)(毫秒)

? ? private final long twepoch;

? ? //數(shù)據(jù)中心最大id

? ? private final long maxDatacenterId;

? ? //機(jī)器最大id

? ? private final long maxWorkerId;

? ? //最大序列號(hào)

? ? private final long maxSequence;

? ? //機(jī)器id左移位數(shù)

? ? private final long workerIdShift;

? ? //數(shù)據(jù)中心id左移位數(shù)

? ? private final long datacenterIdShift;

? ? //毫秒數(shù)左移位數(shù)

? ? private final long timestampLeftShift;

? ? //單次批量生成id的最大數(shù)量

? ? private final int maxBatchCount;

? ? // 序列號(hào)

? ? private long sequence = 0L;

? ? // 上一次時(shí)間戳

? ? private long lastTimestamp = -1L;

? ? public SnowflakeIdGenerator(SnowflakeProperties properties) {

? ? ? ? //數(shù)據(jù)中心id

? ? ? ? this.datacenterId = properties.getDatacenterId();

? ? ? ? //數(shù)據(jù)中心id位數(shù)

? ? ? ? this.datacenterBits = properties.getDatacenterBits();

? ? ? ? //機(jī)器id

? ? ? ? this.workerId = properties.getWorkerId();

? ? ? ? //機(jī)器id位數(shù)

? ? ? ? this.workerBits = properties.getWorkerBits();

? ? ? ? //序列id所占位數(shù)

? ? ? ? this.sequenceBits = properties.getSequenceBits();

? ? ? ? // 時(shí)間戳起始點(diǎn)(毫秒)

? ? ? ? this.twepoch = properties.getTwepoch();

? ? ? ? //數(shù)據(jù)中心最大id

? ? ? ? this.maxDatacenterId = -1L ^ (-1L << properties.getDatacenterBits());

? ? ? ? //機(jī)器最大id

? ? ? ? this.maxWorkerId = -1L ^ (-1L << properties.getWorkerBits());

? ? ? ? //最大序列號(hào)

? ? ? ? this.maxSequence = -1L ^ (-1L << properties.getSequenceBits());

? ? ? ? this.workerIdShift = properties.getSequenceBits();

? ? ? ? //數(shù)據(jù)中心id左移位數(shù)

? ? ? ? this.datacenterIdShift = properties.getSequenceBits() + properties.getWorkerBits();

? ? ? ? //毫秒數(shù)左移位數(shù)

? ? ? ? this.timestampLeftShift = properties.getSequenceBits() + properties.getWorkerBits() + properties.getSequenceBits();

? ? ? ? //單次批量生成id的最大數(shù)量

? ? ? ? this.maxBatchCount = properties.getMaxBatchCount();

? ? ? ? // 校驗(yàn)datacenterId和workerId是否超出最大值

? ? ? ? if (datacenterId > maxDatacenterId || datacenterId < 0) {

? ? ? ? ? ? throw new IllegalArgumentException(String.format("數(shù)據(jù)中心Id不能大于%d或小于0", maxDatacenterId));

? ? ? ? }

? ? ? ? if (workerId > maxWorkerId || workerId < 0) {

? ? ? ? ? ? throw new IllegalArgumentException(String.format("機(jī)器Id不能大于%d或小于0", maxWorkerId));

? ? ? ? }

? ? }

? ? /**

? ? * id生成方法(單個(gè))

? ? * @return

? ? */

? ? public synchronized long nextId() {

? ? ? ? //獲取當(dāng)前時(shí)間的毫秒數(shù)

? ? ? ? long timestamp = currentTime();

? ? ? ? //判斷時(shí)鐘是否回?fù)?/p>

? ? ? ? if (timestamp < lastTimestamp) {

? ? ? ? ? ? throw new RuntimeException(String.format("時(shí)鐘回?fù)芷溃負(fù)芎撩霐?shù):%d", lastTimestamp - timestamp));

? ? ? ? }

? ? ? ? //設(shè)置序列號(hào)

? ? ? ? if (lastTimestamp == timestamp) {

? ? ? ? ? ? //設(shè)置序列號(hào)遞增,如果當(dāng)前毫秒內(nèi)序列號(hào)已經(jīng)達(dá)到最大值洛口,則直到下一毫秒在重新從0開始計(jì)算序列號(hào)

? ? ? ? ? ? sequence = (sequence + 1) & maxSequence;

? ? ? ? ? ? if (sequence == 0) {

? ? ? ? ? ? ? ? timestamp = tilNextMillis(lastTimestamp);

? ? ? ? ? ? }

? ? ? ? } else {

? ? ? ? ? ? sequence = 0L;

? ? ? ? }

? ? ? ? lastTimestamp = timestamp;

? ? ? ? //計(jì)算id

? ? ? ? return ((timestamp - twepoch) << timestampLeftShift) |

? ? ? ? ? ? ? ? (datacenterId << datacenterIdShift) |

? ? ? ? ? ? ? ? (workerId << workerIdShift) |

? ? ? ? ? ? ? ? sequence;

? ? }

? ? /**

? ? * id生成方法(批量)

? ? * @return

? ? */

? ? public synchronized List<Long> nextIds(int count) {

? ? ? ? if (count > maxBatchCount || count < 0) {

? ? ? ? ? ? throw new IllegalArgumentException(String.format("批量生成id的數(shù)量不能大于%d或小于0", maxBatchCount));

? ? ? ? }

? ? ? ? List<Long> ids = new ArrayList<>(count);

? ? ? ? for (int i = 0; i < count; i++) {

? ? ? ? ? ? ids.add(nextId());

? ? ? ? }

? ? ? ? return ids;

? ? }

? ? /**

? ? * 循環(huán)等待直至獲取到新的毫秒時(shí)間戳

? ? * 確保生成的時(shí)間戳總是向前移動(dòng)的矫付,即使在相同的毫秒內(nèi)請(qǐng)求多個(gè)ID時(shí)也能保持唯一性。

? ? */

? ? private long tilNextMillis(long lastTimestamp) {

? ? ? ? long timestamp = currentTime();

? ? ? ? // 循環(huán)等待直至獲取到新的毫秒時(shí)間戳

? ? ? ? while (timestamp <= lastTimestamp) {

? ? ? ? ? ? timestamp = currentTime();

? ? ? ? }

? ? ? ? return timestamp;

? ? }

? ? /**

? ? * 獲取當(dāng)前時(shí)間的毫秒數(shù)

? ? */

? ? private long currentTime() {

? ? ? ? return System.currentTimeMillis();

? ? }

}

這個(gè)Java類SnowflakeIdWorker封裝了雪花算法的核心邏輯第焰。它允許通過構(gòu)造函數(shù)指定數(shù)據(jù)中心ID和機(jī)器ID买优,并提供了nextId()和nextIds()方法用于生成唯一的ID。該方法通過同步關(guān)鍵字synchronized保證了線程安全。

SnowflakeApi

import cn.xj.snowflake.generator.SnowflakeIdGenerator;

import jakarta.annotation.Resource;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController

public class SnowflakeApi {

? ? @Resource

? ? private SnowflakeIdGenerator snowflakeIdGenerator;

? ? @PostMapping("/snowflake/api/nextId")

? ? public Long nextId(){

? ? ? ? return snowflakeIdGenerator.nextId();

? ? }

? ? @PostMapping("/snowflake/api/nextIds")

? ? public List<Long> nextIds(@RequestBody int count){

? ? ? ? return snowflakeIdGenerator.nextIds(count);

? ? }

}

接口調(diào)用詳情

單個(gè)id生成接口nextId:

批量id生成接口nextIds:我們此處生成了10萬條id,響應(yīng)時(shí)長(zhǎng)不到1s

雪花算法的開源代碼或者優(yōu)秀代碼示例有很多杀赢,但思想基本是一樣的烘跺。

雪花算法的缺點(diǎn)

雪花算法在單機(jī)系統(tǒng)上 ID 是遞增的,但強(qiáng)依賴與系統(tǒng)時(shí)間的一致性葵陵,如果系統(tǒng)時(shí)間被回?fù)芑蛘吒淖円狠赡軙?huì)造成 ID 沖突或者重復(fù)。在分布式系統(tǒng)多節(jié)點(diǎn)的情況下脱篙,所有節(jié)點(diǎn)的時(shí)鐘并不能保證不完全同步娇钱,所以有可能會(huì)出現(xiàn)不是全局遞增的情況。

總結(jié)

分布式唯一 ID 的方案有很多绊困,本文主要討論了雪花算法文搂,組成結(jié)構(gòu)大致分為了無效位泼舱、時(shí)間位识埋、機(jī)器位和序列號(hào)位操漠。其特點(diǎn)是自增毯焕、有序逗栽、純數(shù)字組成查詢效率高且不依賴于數(shù)據(jù)庫糠惫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盯另,一起剝皮案震驚了整個(gè)濱河市蛙吏,隨后出現(xiàn)的幾起案子作谭,更是在濱河造成了極大的恐慌稽物,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件折欠,死亡現(xiàn)場(chǎng)離奇詭異贝或,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锐秦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門咪奖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酱床,你說我怎么就攤上這事羊赵。” “怎么了扇谣?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵慷垮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我揍堕,道長(zhǎng)料身,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任衩茸,我火速辦了婚禮芹血,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己幔烛,他們只是感情好啃擦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饿悬,像睡著了一般令蛉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狡恬,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天珠叔,我揣著相機(jī)與錄音,去河邊找鬼弟劲。 笑死祷安,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的兔乞。 我是一名探鬼主播汇鞭,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼庸追!你這毒婦竟也來了霍骄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤淡溯,失蹤者是張志新(化名)和其女友劉穎读整,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體血筑,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年煎楣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豺总。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡择懂,死狀恐怖喻喳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情困曙,我是刑警寧澤表伦,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站慷丽,受9級(jí)特大地震影響蹦哼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜要糊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一纲熏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦局劲、人聲如沸勺拣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽药有。三九已至,卻和暖如春苹丸,著一層夾襖步出監(jiān)牢的瞬間愤惰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工谈跛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羊苟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓感憾,卻偏偏與公主長(zhǎng)得像蜡励,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阻桅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容