唯一 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(已排序) | 機器 | 實際產生於 | 它的時鐘 | 順序 |
|---|---|---|---|---|
| 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 msconst 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 RTT | 1 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 ms | 10 ms | |
|---|---|---|---|---|---|---|
| 資料庫自動遞增 | 64 bits | 要:一個資料庫 | 0 | 0.0% | 0.0% | 0.0% |
| UUID v4 | 128 bits | 不用 | 9.4×10^−20 | 50.1% | 50.1% | 50.1% |
| UUID v7 | 128 bits | 不用 | ≤ 2.6×10^−5 | 0.0% | 3.0% | 10.9% |
| Snowflake | 64 bits | 只在分配機器編號時 | 0 | 0.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 傳給瀏覽器要用字串,否則最後幾位會被悄悄改掉。