跳到主要內容

系統設計

把資料結構放大到好幾台機器

主題 · 唯一 ID 產生

唯一 ID 產生

好幾台機器同時產生 ID,還不能重複:資料庫自動遞增、UUID(Universally Unique Identifier)、Snowflake(時間戳+機器編號+序號),差在能不能依時間排序、ID 有多長、需不需要彼此協調。

做法
3 ms

三台機器(A、B、C)在 36 ms 內共發出 18 個 ID。A 的時鐘慢 3 ms、B 準確、C 快 2 ms。

依 ID 排序
ID(已排序)機器實際產生於它的時鐘順序
A+3 ms−3 ms✓
A+5 ms−3 ms✓
A+10 ms−3 ms✓
A+11 ms−3 ms✓
B+8 ms+0 ms▲ 比上面某個更早產生
B+11 ms+0 ms✓
B+14 ms+0 ms✓
C+17 ms+2 ms✓
A+23 ms−3 ms✓
C+18 ms+2 ms▲ 比上面某個更早產生
C+19 ms+2 ms▲ 比上面某個更早產生
C+21 ms+2 ms▲ 比上面某個更早產生
A+27 ms−3 ms✓
A+36 ms−3 ms✓
B+33 ms+0 ms▲ 比上面某個更早產生
C+31 ms+2 ms▲ 比上面某個更早產生
C+33 ms+2 ms▲ 比上面某個更早產生
B+36 ms+0 ms✓
符號 · 1 位
0
自 2020-01-01 起的毫秒 · 41 位
213451200000 → 12:00:00.000
機器 · 10 位
1
序號 · 12 位
0
順序錯的配對
9 / 150
重複的 ID
0
被拒絕
0
每次都要協調
不用

41 位毫秒、10 位機器編號、12 位序號:只要每台機器的編號不重複,就不用協調也不會撞號,在同一台機器上嚴格遞增。跨機器的順序只和時鐘一樣準:時鐘差 3 ms 時有 9 / 150 對順序錯。

模型:UUID 用固定種子的亂數產生,每次看到的都一樣。「順序錯」是照 ID 排序後,較晚產生的排到前面;同一毫秒產生的配對不計。

亮起來的是這一步執行的程式碼
const EPOCH = 1577836800000n; // 2020-01-01, in Unix ms
const MACHINE_BITS = 10n, SEQUENCE_BITS = 12n;
const MAX_SEQUENCE = (1n << SEQUENCE_BITS) - 1n;
class Snowflake {
private lastMs = -1n;
private sequence = 0n;
constructor(private machine: bigint) {}
// nowMs: this machine's clock, in Unix milliseconds. Null: this
// millisecond's sequence numbers are used up; ask again in the next one.
nextId(nowMs: number): bigint | null {
const now = BigInt(nowMs);
if (now < this.lastMs) {
throw new Error("clock moved backwards; refusing to reuse time");
}
if (now === this.lastMs) {
if (this.sequence === MAX_SEQUENCE) return null;
this.sequence += 1n;
} else {
this.sequence = 0n;
}
this.lastMs = now;
return ((now - EPOCH) << (MACHINE_BITS + SEQUENCE_BITS))
| (this.machine << SEQUENCE_BITS)
| this.sequence;
}
}
function decode(id: bigint): { ms: number; machine: number; sequence: number } {
return {
ms: Number((id >> (MACHINE_BITS + SEQUENCE_BITS)) + EPOCH),
machine: Number((id >> SEQUENCE_BITS) & ((1n << MACHINE_BITS) - 1n)),
sequence: Number(id & MAX_SEQUENCE),
};
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • UUID 隨機數以種子產生以便重現,不能用於安全識別;Snowflake 的 worker ID 須唯一,時鐘回退須依展示策略處理。

什麼時候用

  • 單一資料庫撐得住時,用自動遞增:最短、完全有序、好讀。缺點是會洩漏數量(訂單號 10234 告訴對手你賣了多少),而且分片之後就不再唯一。
  • 到處都能產生、不在乎順序時,用 UUID v4:用戶端離線建立的物件、對外的不可猜測代碼。
  • 要當資料庫主鍵又要分散產生時,用 UUID v7 或 Snowflake:大致照時間排序,新資料集中插在索引尾端。要 64 位元、要知道是哪台機器發的,選 Snowflake;不想管理機器編號,選 UUID v7。

和其他主題的關係

出現在這些架構裡

時間與空間複雜度(Big O)

操作平均最差
自動遞增:發一個 ID
每次都要問資料庫(可以一次拿一批號碼來省)
1 RTT1 RTT
UUID:發一個 ID
本地產生
O(1)O(1)
Snowflake:發一個 ID
同一毫秒用完 4,096 個序號要等下一毫秒
O(1)1 ms
Snowflake:拆解
三次位移與遮罩
O(1)O(1)

空間:O(1),每台機器記上一次的毫秒和序號

和其他做法比

大小每個 ID 都要協調10 億個 ID 撞號機率順序錯(時鐘差 0)3 ms10 ms
資料庫自動遞增64 bits要:一個資料庫00.0%0.0%0.0%
UUID v4128 bits不用9.4×10^−2050.1%50.1%50.1%
UUID v7128 bits不用≤ 2.6×10^−50.0%3.0%10.9%
Snowflake64 bits只在分配機器編號時00.0%4.3%12.1%

撞號機率用生日問題的近似 1 − e^(−n²/2^(b+1)):UUID v4 有 122 個隨機位元;UUID v7 每毫秒內有 74 個隨機位元,表上是把 10 億個全塞進同一毫秒的上限。順序錯是 30 組、每組 24 個 ID 實測的比例。Snowflake 和自動遞增靠結構保證不撞號(前提是機器編號不重複、時鐘不倒退)。

真實世界裡的它

  • Twitter 在 2010 年為推文 ID 設計了 Snowflake;Discord、Instagram 用它的變體。
  • UUID v7 在 2024 年寫進 RFC 9562(RFC,Request for Comments);PostgreSQL 18 內建 uuidv7()。ULID(Universally Unique Lexicographically Sortable Identifier)是同樣想法的早期版本。
  • Flickr 用兩台資料庫各發奇數和偶數號碼,Instagram 在每個分片裡用 PostgreSQL 函式組出 64 位元 ID。

取捨與陷阱

  • 時鐘會倒退:NTP(Network Time Protocol)校時、虛擬機器搬移都可能把時鐘往回撥。Snowflake 必須偵測並等待或拒絕,否則會發出比先前還小、甚至重複的 ID。
  • 機器編號不能重複:兩台機器拿到同一個編號,Snowflake 的唯一性就沒了。編號要由協調服務分配,或用租約綁定。
  • 跨機器的「時間順序」只和時鐘一樣準。要確定因果先後,用邏輯時鐘,不要拿 ID 比大小。
  • JavaScript 的數字只有 53 位元精度:64 位元的 Snowflake 傳給瀏覽器要用字串,否則最後幾位會被悄悄改掉。