分散式系統要在多台機器上生成不重複的 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 = 5 和 workerIdBits = 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 的結構不太一樣: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 5 | 12 bit | 2010-11-04 |
| Discord | 42 bit(ms) | worker 5 + process 5 | 12 bit | 2015-01-01 |
| 41 bit(ms) | shard 13 | 10 bit | 自訂 | |
| Mastodon | 48 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 的差異。
| 面向 | Snowflake | UUIDv7 | ULID |
|---|---|---|---|
| 大小 | 64 bit(8 bytes) | 128 bit(16 bytes) | 128 bit(16 bytes) |
| 儲存欄位型別 | BIGINT | UUID / 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 是更省事的起點。