UUID 會不會撞到 - 因錯誤運維與實作而引來重複的模式
· 更新日期: · 小村 豪 · UUID, 識別碼, 分散式系統, 資料設計, 實作
更新紀錄(僅初版,2026年03月24日 發布)
- 初次發布
引用本文(DOI: 10.5281/zenodo.21616356)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
小村 豪(2026)。〈UUID 會不會撞到 - 因錯誤運維與實作而引來重複的模式〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616356 https://staging.comcomponent.com/zh-TW/blog/2026/03/24/001-uuid-collision-bad-implementation-patterns/
- DOI(最新版本)
- 10.5281/zenodo.21616356
- DOI(此版本)
- 10.5281/zenodo.21616357
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 的思考方式。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
什麼是數位發票(Digital Invoice)?──與「把請款單 PDF 寄送 email」有何不同
數位發票是指將請款資訊從賣方系統直接以資料形式傳遞給買方系統、無需人工介入的機制。本文將淺顯解說它與 PDF 請款單的差異、與日本發票制度(Invoice 制度)的關係、Peppol・JP PINT 的運作方式、與電子帳簿保存法的關聯,以及中小企業的入門方法。
Windows 應用程式的工作列通知區常駐與 Toast 通知 —— NotifyIcon 的陷阱與 AppNotification 的選型
本文整理將業務用 Windows 應用程式常駐於工作列通知區,並透過 Toast 通知告知使用者的實作要點。內容涵蓋 NotifyIcon 的正確用法與「關閉後仍留在通知區」的設計、檔案總管重新啟動時的重新註冊、三種 Toast API(Windows App SDK Ap...
從 WordPress 遷移到 Movable Type ── 正因為是「反方向」,更該整理清楚的實務步驟
從實務角度解說從 WordPress 遷移到 Movable Type(MovableType.net)的步驟。整理遷移合理的情境、文章與固定頁面及圖片的匯入、自訂文章類型的處理方式、透過 URL 設計與 301 轉址延續 SEO 成效,以及外掛功能的替代方案。
VB6 應用程式能用到什麼時候 ── 執行環境的支援現況與務實的 .NET 遷移做法
VB6 應用程式究竟能用到什麼時候?本文整理 VB6 執行環境的支援政策(Windows 11 也在支援範圍內)與 IDE 支援早已終止這種不對稱現況,並以實務指南的形式,說明全面重寫、自動轉換、階段性遷移的判斷表、遷移前的資產盤點、VB6 與 .NET 的不相容之處,以及...
把 FAX 收單搬到網路上 ── 雙軌並行期的設計與分階段轉移實務
說明將 FAX 收單轉移到網路收單或 CSV 匯入的實務作法。整理一次性全面網路化容易失敗的原因、FAX 與網路雙軌並行期的設計、商品與客戶主檔的整備、CSV 匯入這種中間形態,以及如何讓客戶配合的分階段轉移步驟。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 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,絕不能明知情況下回傳重複值。實務結論是不要自製,採用標準函式庫或廣泛使用的實作。