UUID 會不會撞到 - 因錯誤運維與實作而引來重複的模式

· · UUID, 識別碼, 分散式系統, 資料設計, 實作

UUID 明明作為主鍵,某一天卻跑出 duplicate key
這一瞬間,相當高的機率會出現「UUID 最後還是會撞吧」的討論。

不過實務上發生的 UUID 重複,多數並非 UUID 規格本身的問題,而是 實作或運維把規格所預設的生成條件打壞了。RFC 9562 中 UUIDv4 有 122 bits 的隨機區,UUIDv7 則是時間戳之外的 74 bits 也以亂數或 carefully seeded counter 達成唯一性。另一方面,UUIDv8 被明確寫為「實作相依,不應以唯一性為前提」。123
此外,Python 標準函式庫的 uuid4() 也說明會以密碼學安全的方式生成,至少「正常使用品質可靠的實作」時,UUID 這一邊的前提是相當強的。4

本文整理 因錯誤運維或實作讓 UUID 撞到的典型模式,並附上對應的防再發策略。
內容根據 2026 年 3 月時點 可查閱的 RFC 9562、Python 官方文件、PostgreSQL 官方文件。546

1. 先下結論

簡短整理,危險的模式如下。

模式 會發生什麼 應先做的對策
用固定 seed 或薄弱的 PRNG 自行做出像 UUIDv4 的值 其他行程或其他節點重現同一序列 使用 OS/runtime 標準的 UUID API
fork、VM snapshot、容器複製之後仍沿用生成狀態 亂數或 counter 狀態倒退,產生重複 fork 後重新 seed、clone 後重新初始化、重新審視持久狀態的處理
誤把 UUIDv3 / v5 當成「每次都全新的 ID」 同一 namespace 與同一 name 會再次生出同一 UUID 理解它是決定性 ID,限定其用途
自行實作 UUIDv1 / v6 / v7 / v8,對 clock rollback 或 node/counter 處理馬虎 高頻率生成或多節點時容易重複 使用既有函式庫,減少自製生成器
中途把 UUID 截短或壓縮成別的格式 自行拋棄原本 128 bits 的唯一性 儲存/比較以完整長度進行
DB 側沒有 UNIQUE / PRIMARY KEY 重複悄悄混入,原因調查落後 在儲存層持有唯一約束

簡言之,多數情況與其說是 UUID 撞了,不如說是 在中途的設計裡把原本對 UUID 期望的唯一性削掉了

2. 先懷疑的是「生成與運維」,不是「UUID 的數學」

UUID 的討論會變複雜,是因為不同版本的性質各異。

  • UUIDv4 以亂數為主。RFC 9562 中,除 version/variant 外共 122 bits 由亂數填滿。1
  • UUIDv7 結構利於時序排序,在 Unix 毫秒時間戳之外,其餘由亂數或 carefully seeded counter 構成。2
  • UUIDv3 / v5 是 name-based,在相同 namespace 與相同 canonical name 下會輸出相同 UUID,這是正確行為。7
  • UUIDv8 是實驗用/廠商自訂用,唯一性由實作決定,RFC 明寫「不應以唯一性為前提」。3

所以即便說「在用 UUID」,裡頭究竟是

  • 標準函式庫的 uuid4()
  • 自製的 timestamp + random
  • uuid5(namespace, name)
  • 僅外觀像 UUIDv8 的自訂格式

每種情境話題完全不同。

flowchart TD
    A[發現 UUID 重複] --> B{究竟是在哪裡變成相同的值}
    B --> C[生成器薄弱]
    B --> D[狀態倒退]
    B --> E[誤用 name-based UUID]
    B --> F[儲存時截短]
    B --> G[DB 側缺少唯一約束]
    C --> H[實作失誤]
    D --> H
    E --> H
    F --> H
    G --> H

實務上,從這張圖的右側往左看會比較快找到原因。

3. 模式 1:自稱是 UUIDv4,但實際用了薄弱的 PRNG

最常見的就是這個。

  • Math.random() 這類通用 PRNG 湊 128 bits
  • 啟動時以 time() 或 PID 塞 seed
  • 自行拼出「看起來是 UUID 的 32 hex 字元」

外觀是 UUID,但 亂數來源薄弱的話,另一個行程或另一個節點就會重現相同序列

RFC 9562 為了 UUID 的唯一性與不可預測性,要求 應使用 CSPRNG,更明確要求 遇到 process fork 等狀態變化時,要對 CSPRNG 的狀態做適當的 reseed8
Python 的 uuid.uuid4() 也說明會以密碼學安全的方式生成亂數 UUID。4

這裡的實務結論相當單純。

  • 不要自製 UUID
  • 不要手動操作亂數 seed
  • 採用標準函式庫或廣泛使用的實作

「因為輕量」「因為以前就這樣用」而保留自製生成器,未來付出的代價最大。

4. 模式 2:fork、snapshot、clone 把生成狀態倒退

第二危險的,是 讓生成器狀態被複製或倒退的運維方式

RFC 9562 明確建議 fork 後要 reseed,也說明未持有 stable storage 的實作 clock sequence、counter、random data 的生成頻率會上升,讓重複機率提高89

從這裡自然可以推出實務上的注意點。

  • 取 VM snapshot 後用同一個映像復原好幾份
  • 容器映像啟動時,自製生成器從相同初始狀態開始
  • worker fork 後,共享了 PRNG 或 counter 狀態

在這些運維下,UUID 生成序列會在非預期下被重現
RFC 雖然沒直接寫「snapshot 危險」,但從 fork 後 reseed 與 generator state 處理的警告,就能推出這項相當實務的注意點。89

對策如下。

  • 不要長久保留自製的 UUID 生成狀態
  • fork / clone / restore 之後立刻重新初始化
  • 可能的話偏向每次都使用 OS 提供亂數的實作
  • 若是高頻生成器,把狀態管理與 reseed 規格明文化

5. 模式 3:把 UUIDv3 / v5 誤解成「每次都新的 ID」

UUIDv3 / v5 並非不易撞碼的隨機 ID。
它是 能從相同名稱重新生出相同 ID 的決定性 ID

RFC 9562 明寫 相同 canonical format 的 same name 在 same namespace 下生成的 UUID 必須相等7
也就是說下列用法出現的重複並不是事故,而是規格使然。

  • uuid5(NAMESPACE_URL, "https://example.com/users/42") 每次當作「新發號」
  • 不把 tenant 放進 namespace,用全客戶共用的 namespace + email 發號
  • 以為同一個邏輯名稱在 retry 時會產生不同 ID

反過來,若 name 的 canonicalization 沒統一,同一個對象會變成不同 UUID。RFC 對 canonical representation 的處理也強調得很重。710

在這一類情境中重要的是:

  • UUIDv3 / v5 不是「不會撞碼的發號」,而是「同輸入就同 ID」
  • 不要讓 namespace 的設計模糊
  • 把 name 的 canonicalization 規格化

這三點。

6. 模式 4:自行實作時間系 UUID 或 UUIDv8

UUIDv1 / v6 / v7 / v8 只模仿外觀相當危險

6.1 在 UUIDv1 / v6 中對 node 或 clock sequence 馬虎

RFC 9562 中,UUIDv6 是為了 DB locality 把 UUIDv1 重新排序得到的版本,涉及 clock sequence 或 node。並且關於分散式環境下的 node collision resistance 與狀態保留,也有多項警告。11912

RFC 甚至明寫 因為虛擬機與容器的出現,MAC 位址的唯一性已不再保證5

所以像這樣的設計都危險。

  • 認定「MAC 位址一定唯一」
  • 把 node ID 一起烙進映像檔複製
  • 每次重啟都把 clock sequence 重設到固定值

6.2 自行實作 UUIDv7,卻放著 counter rollover 或 clock rollback 不管

UUIDv7 相當實用,但 RFC 對高頻生成時的 monotonicity 與 counter handling 描述得很仔細,並明確要求 絕不能在 clock rollback 或 counter rollover 時 knowingly return 重複值213

也就是說,下列實作都危險。

  • 同一毫秒內要大量發號,卻沒有 counter 設計
  • 時鐘倒退時仍然照常繼續生成
  • 多個行程各自分別初始化同一個 internal counter

6.3 以為 UUIDv8 是「新的 UUID 規格」就輕率使用

UUIDv8 看起來好用,但 RFC 9562 講得很直白,UUIDv8 的唯一性由實作決定,不應以唯一性為前提3

也就是說,下列做法打造出的「自家 UUID」,它的設計文件本身就是 UUID 的唯一性規格

  • 嵌入 timestamp
  • 嵌入 shard id
  • 嵌入某種業務含義
  • 剩下隨便塞 random

沒經過審查就導入,相當危險。

7. 模式 5:中途把 UUID 變短

就算生成階段沒錯,儲存或比較階段也可能把它打壞。

典型例如下。

  • 只取前 8 個字元當外鍵
  • 把 128 bits 的 UUID 壓成 64 bits 整數
  • 字串欄位長度不足導致結尾被截
  • 日誌或畫面顯示的縮短表示直接被當成唯一鍵使用

要留意的是,換一種表現本身並不壞

  • 去掉 hyphen
  • 統一大小寫
  • 以 16 bytes 二進位保存

這類 不丟失 128 bits 的轉換 沒問題。
危險的是 把唯一性本身的素材削掉的轉換

尤其是「為了讓人好看而額外做的縮短 ID」不知不覺變得比原本的 UUID 更具優先地位的設計,特別容易出事故。

8. 模式 6:DB 側沒有唯一約束

最後這項也很關鍵。

就算 UUID 已經夠難撞,如果真的無法容忍重複,儲存端就應該有唯一約束

PostgreSQL 官方文件說明,unique constraint 保證一個欄位或欄位集合的值在整張表內唯一,primary key 則是 unique 且 not null 的列識別子。6

RFC 9562 也指出,UUID 實作上可以提供充分的唯一性,但 絕對無法保證真正的 global uniqueness,且在 collision impact 高的用途下,應該採取更強的對策。14

所以實務上,基本組合是:

  • 把 UUID 用作不易撞碼的 ID
  • DB 以 UNIQUE / PRIMARY KEY 作為最後防線
  • 設計重複時的 retry / idempotency / incident logging

「用 UUID」與「不放唯一約束」並不是同義。

9. 實務查核清單

整理成可直接用於導入或稽核的形式。

  1. 確認沒有自製 UUID 生成器
    能換成 uuid4() / uuid7() 這類標準 API 的,就盡量換過去。
  2. 把 UUID 的 version 當作規格決定
    v4/v7 是亂數系、v3/v5 是決定性、v8 是自訂規格,明寫清楚。
  3. 盤點 seed 與 generator state 的處理
    fork、worker 重啟、snapshot、clone 之後,不要讓它們沿用同一狀態。
  4. 確認儲存時仍維持完整長度
    不要把 prefix 比較或縮短顯示當成真正的鍵使用。
  5. 在 DB 放 UNIQUE / PRIMARY KEY
    UUID 只是降低機率的機制,本身不是約束。
  6. 讓重複可被觀測
    不要把 duplicate key 壓下去,要能追到是哪個 generator/node/deployment 造成的。

10. 總結

UUID 的撞碼事故,多半不是 UUID 本身薄弱,而是 實作或運維把 UUID 的前提打壞 所造成。

  • 用薄弱亂數自製
  • fork 或 snapshot 後讓狀態倒退
  • 把 name-based UUID 拿來當發號用
  • 自行輕率實作 v7 或 v8
  • 中途截短把唯一性丟掉
  • 拿掉 DB 側的唯一約束

做了這些,其實不是「UUID 撞了」,而更像是 從我們這邊主動製造容易撞的情境

發現重複時,比起先懷疑 UUID 的數學,更該先看的是 生成器、狀態管理、保存格式、約束設計
按這個順序看,原因通常會縮得很快。

11. 相關文章

12. 參考資料

  1. IETF RFC 9562, Section 5.4 UUID Version 4. 關於 UUIDv4 的 122 bits 亂數區。  2

  2. IETF RFC 9562, Section 5.7 UUID Version 7. 關於 UUIDv7 的 timestamp、random bits、counter 設計思路。  2 3

  3. IETF RFC 9562, Section 5.8 UUID Version 8. 關於 UUIDv8 的唯一性由實作決定,不應以其為前提。  2 3

  4. Python 3.14 documentation, uuid module. 關於 uuid4() 的 cryptographically-secure 生成、uuid5() 的 deterministic 行為、uuid7() / uuid8() 的性質。  2 3

  5. IETF RFC 9562, Universally Unique IDentifiers (UUIDs). UUID 格式、各版本、整體 best practices 的基準文件。  2

  6. PostgreSQL documentation, Constraints. 關於 UNIQUE 約束與 PRIMARY KEY 對唯一性的保障。  2

  7. IETF RFC 9562, Section 6.5 Name-Based UUID Generation. 關於相同 namespace + 相同 name 會產出同一 UUID,以及 canonicalization 的重要性。  2 3

  8. IETF RFC 9562, Section 6.9 Unguessability. 關於 CSPRNG 的使用與 fork 後的 reseed。  2 3

  9. IETF RFC 9562, Section 6.3 UUID Generator States. 關於 stable storage 與 generator state 的處理。  2 3

  10. IETF RFC 9562, Section 5.5 UUID Version 5. 關於基於 namespace + canonical name 的 name-based UUID 規格。 

  11. IETF RFC 9562, Section 5.6 UUID Version 6. 關於 UUIDv6 的 node / clock sequence / DB locality。 

  12. IETF RFC 9562, Section 6.4 Distributed UUID Generation. 關於分散式環境下的 node collision resistance。 

  13. IETF RFC 9562, Section 6.2 Monotonicity and Counters. 關於 clock rollback、counter rollover、batch generation 的注意事項。 

  14. IETF RFC 9562, Sections 6.7 and 6.8. 關於 collision resistance 與 global uniqueness 的思考方式。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

偽隨機數與真正隨機數的差異 - 如何區分的整理

本文整理偽隨機數與真正的隨機數差異,重點不在輸出外觀而在產生器結構:一般 PRNG 重視可重現性、CSPRNG / DRBG 主打不可預測性、NRBG 則以物理熵源為基礎。文中說明 seed 與 reseed、health test 的角色,並給出資安、模擬、抽籤等用途的選...

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽