分散式唯一 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 位元編碼時間與機器資訊,本地產生、可排序、純數字

方案位元數唯一性可排序純數字協調需求
自增視欄位單點/高
UUID128
Snowflake64低(僅機器號)

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 起算的毫秒數
資料中心 ID52^5 = 32 個資料中心
機器 ID52^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 個
去中心化只需預先分配機器號,產生時無需協調