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 的狀態做適當的 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. 實務查核清單
整理成可直接用於導入或稽核的形式。
- 確認沒有自製 UUID 生成器
能換成uuid4()/uuid7()這類標準 API 的,就盡量換過去。 - 把 UUID 的 version 當作規格決定
v4/v7 是亂數系、v3/v5 是決定性、v8 是自訂規格,明寫清楚。 - 盤點 seed 與 generator state 的處理
fork、worker 重啟、snapshot、clone 之後,不要讓它們沿用同一狀態。 - 確認儲存時仍維持完整長度
不要把 prefix 比較或縮短顯示當成真正的鍵使用。 - 在 DB 放 UNIQUE / PRIMARY KEY
UUID 只是降低機率的機制,本身不是約束。 - 讓重複可被觀測
不要把 duplicate key 壓下去,要能追到是哪個 generator/node/deployment 造成的。
10. 總結
UUID 的撞碼事故,多半不是 UUID 本身薄弱,而是 實作或運維把 UUID 的前提打壞 所造成。
- 用薄弱亂數自製
- fork 或 snapshot 後讓狀態倒退
- 把 name-based UUID 拿來當發號用
- 自行輕率實作 v7 或 v8
- 中途截短把唯一性丟掉
- 拿掉 DB 側的唯一約束
做了這些,其實不是「UUID 撞了」,而更像是 從我們這邊主動製造容易撞的情境。
發現重複時,比起先懷疑 UUID 的數學,更該先看的是 生成器、狀態管理、保存格式、約束設計。
按這個順序看,原因通常會縮得很快。
11. 相關文章
12. 參考資料
-
IETF RFC 9562, Section 5.4 UUID Version 4. 關於 UUIDv4 的 122 bits 亂數區。 ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. 關於 UUIDv7 的 timestamp、random bits、counter 設計思路。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.8 UUID Version 8. 關於 UUIDv8 的唯一性由實作決定,不應以其為前提。 ↩ ↩2 ↩3
-
Python 3.14 documentation,
uuidmodule. 關於uuid4()的 cryptographically-secure 生成、uuid5()的 deterministic 行為、uuid7()/uuid8()的性質。 ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). UUID 格式、各版本、整體 best practices 的基準文件。 ↩ ↩2
-
PostgreSQL documentation, Constraints. 關於 UNIQUE 約束與 PRIMARY KEY 對唯一性的保障。 ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. 關於相同 namespace + 相同 name 會產出同一 UUID,以及 canonicalization 的重要性。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. 關於 CSPRNG 的使用與 fork 後的 reseed。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. 關於 stable storage 與 generator state 的處理。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. 關於基於 namespace + canonical name 的 name-based UUID 規格。 ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. 關於 UUIDv6 的 node / clock sequence / DB locality。 ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. 關於分散式環境下的 node collision resistance。 ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. 關於 clock rollback、counter rollover、batch generation 的注意事項。 ↩
-
IETF RFC 9562, Sections 6.7 and 6.8. 關於 collision resistance 與 global uniqueness 的思考方式。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
整理 Windows 的字元編碼與換行符 - Shift_JIS / UTF-8 / UTF-16、亂碼、CRLF / LF,為何混亂
本文整理 Windows 上字元編碼與換行符容易混亂的核心:bytes、UTF-8 與 CP932、UTF-16LE、BOM、CRLF 與 LF 是不同軸的概念,亂碼源於以錯誤前提 decode,且誤儲存後無法還原。讀完即可在規格中寫出明確的 encoding 與換行約定,...
偽隨機數與真正隨機數的差異 - 如何區分的整理
本文整理偽隨機數與真正的隨機數差異,重點不在輸出外觀而在產生器結構:一般 PRNG 重視可重現性、CSPRNG / DRBG 主打不可預測性、NRBG 則以物理熵源為基礎。文中說明 seed 與 reseed、health test 的角色,並給出資安、模擬、抽籤等用途的選...
GS1 等條碼規格中,標準定義了什麼、實務運用上要注意什麼 - GTIN · AI · GS1-128 · GS1 DataMatrix 的整理
整理 GS1 條碼規格中被標準化的範圍,把 GTIN、AI、GS1-128、GS1 DataMatrix、GS1 QR Code 的差異與適用場景分層說明,並彙整 FNC1、掃描器輸出、商品主檔運用等接收端容易踩雷的重點,協助讀者把條碼當作可共享的業務介面來設計。
開發 COM 元件、OCX/ActiveX 時常見的坑 - 整理 Visual Studio 的 32bit/64bit、註冊、管理員權限
整理開發 COM、OCX、ActiveX 元件時最容易卡關的四個面向:宿主行程的 32bit/64bit、Visual Studio 2022 變成 64bit 後的設計時整合、regsvr32 與 Regasm 的註冊位置、以及管理員權限與 HKCU/HKLM 的關係,協...
應該在哪裡 `catch` 例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責
本文整理在呼叫階層中應該於哪一層 catch 例外、輸出主日誌與進行錯誤處理的實務判斷標準。深層 helper 不寬泛接捕,外部 I/O 邊界負責翻譯例外,UseCase 把預期內失敗結果化,UI 與請求邊界輸出 1 次主日誌並決定回應,未處理例外處理器只承擔最終記錄與終止...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。