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

· 更新日期: · · UUID, 識別碼, 分散式系統, 資料設計, 實作

更新紀錄(1 筆,最後更新 2026年09月03日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。 查看更新前的版本 (DOI: 10.5281/zenodo.21616357)
初次發布
引用本文(DOI: 10.5281/zenodo.21616356)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

小村 豪(2026)。〈UUID 會不會撞到 - 因錯誤運維與實作而引來重複的模式〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616356 https://comcomponent.com/zh-TW/blog/2026/03/24/001-uuid-collision-bad-implementation-patterns/

DOI(最新版本)
10.5281/zenodo.21616356
DOI(此版本)
10.5281/zenodo.22279516

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 的自訂格式

每種情境話題完全不同。

發現 UUID 重複究竟是在哪裡變成相同的值生成器薄弱狀態倒退誤用 name-based UUID儲存時截短DB 側缺少唯一約束實作失誤

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

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

最常見的就是這個。

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

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

RFC 9562 為了 UUID 的唯一性與不可預測性,要求 應使用 CSPRNG,更明確要求 遇到 process fork 等狀態變化時,要對 CSPRNG 的狀態做適當的 reseed。8
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 的思考方式。 ↩

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

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

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

UUID 真的會重複嗎?
規格本身的碰撞機率極低,RFC 9562 中 UUIDv4 有 122 bits 的隨機區。實務上發生的重複多數不是 UUID 數學的問題,而是實作或運維打壞了生成前提,例如用薄弱 PRNG 自製、fork 或 snapshot 後狀態倒退、誤用 name-based UUID、儲存時截短等。發現重複時應先檢查生成器、狀態管理、保存格式與約束設計。
為什麼 UUIDv3 / v5 會產生相同的值?
這是規格使然,不是事故。RFC 9562 明寫相同 namespace 與相同 canonical name 生成的 UUID 必須相等,它是能從相同名稱重新生出相同 ID 的決定性 ID,不是每次都全新的發號機制。把 uuid5 每次當作新發號使用,出現重複是正常行為。反過來,若 name 的 canonicalization 沒統一,同一個對象也會變成不同 UUID。
用了 UUID 還需要資料庫的唯一約束嗎?
需要。RFC 9562 指出 UUID 實作上可以提供充分的唯一性,但絕對無法保證真正的 global uniqueness,在碰撞影響大的用途下應採取更強的對策。實務上的基本組合是:UUID 作為不易撞碼的 ID,資料庫以 UNIQUE 或 PRIMARY KEY 作為最後防線,並設計重複時的 retry、idempotency 與事故記錄。
自己實作 UUID 生成器有什麼風險?
用 Math.random() 這類薄弱 PRNG 湊出來的值,其他行程或其他節點可能重現同一序列。RFC 9562 要求使用 CSPRNG,並在 process fork 等狀態變化時對其狀態適當 reseed;自製 UUIDv7 還必須處理 clock rollback 與 counter rollover,絕不能明知情況下回傳重複值。實務結論是不要自製,採用標準函式庫或廣泛使用的實作。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽