分散式唯一 ID 產生器#
在分庫分表、跨資料中心的系統裡,無法再依賴單一資料庫的自增主鍵。我們需要一個分散式的發號器:產生全域唯一、可大致按時間排序、純數字、64-bit、高吞吐的 ID。本節比較三種主流方案,並深入 Snowflake 的位元配置與時鐘回撥問題。
分散式 ID 的理論定位(為何需要、與分散式時間的關係)見 分散式系統基礎 — 分散式 ID 生成;短網址系統 的發號器也用到相同思路。
需求#
- 唯一:全系統不重複。
- 可排序:大致按時間遞增(同日內較晚產生的 ID 較大),方便範圍查詢與分頁。
- 純數字:只含數字。
- 64-bit:必須塞進 64 位元。
- 高吞吐:至少每秒上萬個 ID。
方案比較#
1. 資料庫自增 (auto_increment)#
- 單點瓶頸:單一資料庫成為效能與可用性瓶頸。
- 難以擴展:多主複製要處理 ID 衝突與同步複雜度。
- 跨資料中心困難:很難跨多區域維持全域遞增。
2. UUID#
- 128-bit,各節點獨立產生、無需協調。
- 碰撞機率極低(以 10 億/秒產生,約 100 年才有 50% 機率出現一次重複)。
- 缺點:不符合 64-bit、不可排序、非純數字、佔用儲存大(當主鍵會讓索引膨脹)。
3. Snowflake#
業界最常用的折衷方案,用 64 位元編碼時間與機器資訊,本地產生、可排序、純數字。
| 方案 | 位元數 | 唯一性 | 可排序 | 純數字 | 協調需求 |
|---|---|---|---|---|---|
| 自增 | 視欄位 | 是 | 是 | 是 | 單點/高 |
| UUID | 128 | 是 | 否 | 否 | 無 |
| Snowflake | 64 | 是 | 是 | 是 | 低(僅機器號) |
Snowflake 位元配置#
flowchart LR
A["符號位<br/>1 bit"] --> B["時間戳<br/>41 bits (ms)"]
B --> C["資料中心 ID<br/>5 bits"]
C --> D["機器 ID<br/>5 bits"]
D --> E["序號<br/>12 bits"]| 欄位 | 位元數 | 說明 |
|---|---|---|
| 符號位 | 1 | 恆為 0(保留,確保正數) |
| 時間戳 | 41 | 自訂 epoch 起算的毫秒數 |
| 資料中心 ID | 5 | 2^5 = 32 個資料中心 |
| 機器 ID | 5 | 2^5 = 32 台機器/資料中心 |
| 序號 | 12 | 同一毫秒內遞增,每毫秒重置 |
合計 1 + 41 + 5 + 5 + 12 = 64 bits。
資料中心 + 機器 ID 常合稱「worker ID」共 10 bits;有些變體直接用 10 bits 的 worker ID 不再細分。兩種寫法等價,重點是「時間在高位 ➡️ 天然按時間排序」。
容量計算#
時間戳壽命(41 bits):
$$ \frac{2^{41}\ \text{ms}}{1000 \times 86400 \times 365} \approx 69\ \text{年} $$
可用自訂 epoch(貼近上線日期)來延長實際可用期。
單機吞吐(12 bits 序號):
$$ 2^{12} = 4096\ \text{個/ms} ;\Rightarrow; 4096 \times 1000 = 4{,}096{,}000\ \text{個/秒/機} $$
遠超「每秒上萬」的需求。
機器規模(10 bits worker ID):
$$ 2^{10} = 1024\ \text{台機器}\quad(32 \times 32) $$
時鐘回撥處理#
Snowflake 假設時間單調遞增,但 NTP 校時或多核時鐘漂移可能讓系統時間往回跳 (clock-back / 時鐘回撥),導致產生重複或更小的 ID。
- 偵測:記錄上次產生 ID 的時間戳;若當前時間 < 上次時間,代表發生回撥。
- 處理策略:
- 拒絕並等待:回撥幅度小,自旋等到時鐘追上再發號。
- 直接拒絕:回撥幅度大,拋例外讓上層處理。
- 用 NTP 平滑校時,避免時間瞬間倒退。
時鐘回撥是 Snowflake 在生產環境最容易踩的坑。發號服務必須持久化「上次時間戳」並嚴格比對,否則 ID 唯一性會被破壞。
小結#
| 需求 | Snowflake 如何滿足 |
|---|---|
| 唯一 | 時間戳 + 機器號 + 序號的組合 |
| 可排序 | 時間戳放在高位 |
| 純數字 / 64bit | 位元打包成一個 long |
| 高吞吐 | 每機每毫秒 4096 個 |
| 去中心化 | 只需預先分配機器號,產生時無需協調 |