分散式系統要在多台機器上生成不重複的 ID,最直覺的辦法是引入一個中央發號服務——所有機器都來這裡拿號碼,保證不撞。這個辦法可靠,但發號服務本身會變成單點,流量大時也容易成為瓶頸。

Twitter 2010 年面對這個問題時,設計出 Snowflake ID:一個 64-bit 整數,靠時間戳與機器編號的組合達成「各自生成、不撞號」,同時讓 ID 天生按時間大致排序。這個設計後來被 Discord、Instagram、Mastodon 等大型服務各自改造沿用,也衍生出各種變體。

位元結構

Twitter 原始 Snowflake 的結構是 64 bit,切成四個部分:

  • 符號位(1 bit):固定為 0,確保 ID 是正整數
  • 時間戳(41 bit):毫秒精度,從 Twitter 自訂的 epoch(2010-11-04 01:42:54 UTC)起算
  • datacenter ID(5 bit):最多 32 個資料中心
  • worker ID(5 bit):每個資料中心最多 32 台機器
  • 序號(12 bit):同一毫秒內最多 4096 個 ID,超過就等到下一毫秒

網路上不少文章把結構寫成「41 + 10-bit machine id + 12」,把 datacenter 和 worker 合併成一個 10-bit 欄位。在「總共 1024 台機器」的結論上是等價的,但 Twitter 的原始程式碼(IdWorker.scala)確實把兩個維度拆開,分別是 datacenterIdBits = 5workerIdBits = 5。後來很多衍生函式庫(如 Go 的 bwmarrin/snowflake)把它們合併成單一 node ID,這才是「10-bit 機器」說法的來源。

理論上限

以原始結構計算:

  • 最大機器數:210 = 1024(32 個資料中心 × 32 台機器)
  • 每毫秒每機器:212 = 4096 個 ID
  • 每秒每機器:約 409 萬個 ID
  • 時間戳跨度:241 毫秒 ≈ 69.7 年,從 Twitter epoch 2010 年起算,大約在 2080 年用盡

常見誤植是寫「2089 年溢位」,那是從 1970 Unix epoch 起算的結果。Twitter 的自訂 epoch 是 2010 年,所以正確上限是 2080 年前後。不同變體的 epoch 各異,溢位時間也跟著不同(Discord 從 2015 年起算,上限約在 2084 年)。

各家變體

Twitter 的原始 Snowflake 專案已於 2021 年封存(archived),不再維護。但這個設計被廣泛借鑒,各服務根據自己的需求調整了欄位配置。

Discord

Discord 把時間戳擴到 42 bit(Twitter 是 41),epoch 改為 2015-01-01 00:00:00 UTC,對應 Discord 服務上線的年份。剩餘部分是 5 bit worker ID + 5 bit process ID + 12 bit 序號。每個 worker-process 組合各自維護序號,不需要跨程序協調。

Instagram

Instagram 的結構不太一樣:41 bit 時間戳 + 13 bit shard ID + 10 bit 序號。shard ID 對應 PostgreSQL 的邏輯分片,把「機器維度」換成「資料分片維度」,序號因此只有 10 bit,每毫秒每個分片最多 1024 個 ID。

Mastodon

Mastodon 走的是另一個方向:48 bit 毫秒時間戳(直接用 Unix epoch,不自訂)+ 16 bit 序號,沒有 machine/worker ID 欄位。ID 在資料庫端(PostgreSQL 函式)生成,16 bit 序號用刻意打散的方式填入,目的是隱藏資料表的實際筆數,不讓外部從 ID 推算活躍度。

變體時間戳機器/分片序號Epoch
Twitter(原始)41 bit(ms)datacenter 5 + worker 512 bit2010-11-04
Discord42 bit(ms)worker 5 + process 512 bit2015-01-01
Instagram41 bit(ms)shard 1310 bit自訂
Mastodon48 bit(ms)16 bit(打散)Unix 1970

機器 ID 協調:主要的運維負擔

Snowflake 的核心假設是「每台機器有唯一的 worker ID」。只要 ID 不撞,即使兩台機器在同一毫秒生成序號相同的 ID,最終結果也不會重複——靠的就是 worker ID 這個差異。

問題是,誰來保證 worker ID 唯一?常見做法有幾種:

  • 設定檔寫死:每台機器部署時手動指定 ID,管理上最簡單,但容器化部署時擴縮容就麻煩了,複製一份設定容易搞出重複
  • ZooKeeper 或 etcd 領號:啟動時向協調服務申請一個空閒 ID,自動分配;代價是多了一個外部依賴
  • 資料庫序號:在資料庫佔用一個序號欄位,每台機器啟動時搶一個;簡單但會增加資料庫負擔
  • IP / 容器 ID 雜湊:用 IP 位址或容器識別碼推算出 worker ID;省去協調步驟,但碰撞風險要仔細評估

機器 ID 協調沒有一個通用的完美解法。這正是 UUIDv7 與 ULID 相對於 Snowflake 的主要優勢:靠隨機位元達成唯一,完全不需要協調。

Snowflake 與 UUIDv7、ULID 的取捨

UUID 版本演進與 ULID 完整指南裡已經整理過 UUIDv7 和 ULID 的結構,這裡只比較它們跟 Snowflake 的差異。

面向SnowflakeUUIDv7ULID
大小64 bit(8 bytes)128 bit(16 bytes)128 bit(16 bytes)
儲存欄位型別BIGINTUUID / BINARY(16)UUID / BINARY(16)
機器 ID 協調需要不需要不需要
時間排序是(毫秒)是(毫秒)是(毫秒)
標準化無統一標準IETF RFC 9562(2024)社群規範(無 RFC)
字串表示純整數數字36 字元(含連字號)26 字元(Base32)
  • 選 Snowflake 的情況:主鍵欄位要用 BIGINT、節省空間;系統有固定的機器或節點數量、協調機制已有現成基礎設施(如 ZooKeeper);或是想在 ID 裡帶機器或分片語意。
  • 選 UUIDv7 的情況:不想處理 worker ID 協調;需要跨系統全域唯一;語言或資料庫已有原生支援(如 PostgreSQL 18 的 uuidv7())。
  • 選 ULID 的情況:偏好較短、人眼友善的字串格式;專案已有成熟的 ULID 工具鏈。

Snowflake 整數主鍵在 JOIN 效率與索引大小上有天然優勢——8 bytes vs 16 bytes,在大資料表上累積起來的差距不小。但如果系統規模不到需要考慮這個差距的程度,免協調的 UUIDv7 通常更省事。

各語言實作範例

Java

華語圈最常用的工具庫 Hutool 內建 Snowflake 實作,直接拿來用不用額外引入依賴(如果專案已有 Hutool 的話):

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;

// datacenterId: 0–31,workerId: 0–31
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
long id = snowflake.nextId();

// 可以從 ID 反推時間戳(毫秒)
long timestamp = snowflake.getGenerateDateTime(id);

Go

bwmarrin/snowflake 把 datacenter + worker 合併成單一 10-bit node ID(0–1023),是 Go 生態中使用廣泛的實作:

import "github.com/bwmarrin/snowflake"

// nodeID: 0–1023
node, err := snowflake.NewNode(1)
if err != nil {
    panic(err)
}

id := node.Generate()
fmt.Println(id.Int64())   // 64-bit 整數
fmt.Println(id.String())  // 字串形式
fmt.Println(id.Time())    // 生成時間(毫秒)

Sony 的 sonyflake 是另一個常見的 Go 變體:時間單位改為 10 毫秒、machine ID 16 bit(更多機器)、序號縮到 8 bit。適合機器數量多、對 ID 生成速率需求相對低的場景。

Node.js

nodejs-snowflake 核心用 Rust 編譯成 WebAssembly,在 Node.js 的單執行緒環境下也能安全處理序號遞增:

import { UniqueID } from "nodejs-snowflake";

const uid = new UniqueID({
  returnNumber: false,   // 回傳字串(64-bit 整數超過 JS Number 安全範圍)
  machineID: 1,          // 0–1023
  customEpoch: 1609459200000,  // 自訂 epoch(2021-01-01)
});

const id = uid.getUniqueID();
const timestamp = uid.getTimestampFromID(id);  // 反推時間戳

注意 JavaScript 的 Number 只能安全表示 53 bit 整數(Number.MAX_SAFE_INTEGER),Snowflake 的 64-bit ID 超出這個範圍。在 Node.js 端要用字串或 BigInt 處理,存進資料庫時也要確認 ORM 不會把它強轉成 Number 丟失精度。

小結

Snowflake 解決的核心問題是:在分散式環境下生成 64-bit 整數 ID,不需要中央發號、ID 大致照時間遞增。它的設計被許多大型服務借鑒,但各家都按自己的需求調整了 epoch 和欄位配置,沒有統一版本。

Snowflake 最值得考慮的情況是需要 BIGINT 主鍵、系統有明確的機器邊界、且已有基礎設施能處理 worker ID 協調。如果系統規模沒大到需要計較 8 bytes vs 16 bytes 的差距,或是不想引入協調複雜度,UUIDv7 是更省事的起點。


Sponsored Links

發佈留言