UrsaKit 封面圖:對比 UUID v4 的隨機位元與 UUID v7 的時間戳優先結構

UUID 是 128 位元識別碼,寫成五組、共 32 個十六進位字元。第 4 版幾乎整串都是隨機資料;第 7 版則把 Unix 毫秒時間戳放在最高位,因此識別碼會依建立的瞬間排序。

這個差異在 2024 年 5 月成為標準問題:RFC 9562 正式納入第 6、7、8 版,並取代原本的 RFC 4122。本文比較你真正會在新項目裡二選一的兩個版本,示範怎麼從 v7 還原建立時間,也說明什麼情況下 v7 是錯的答案。

01RFC 9562 改了什麼

RFC 4122 定義了多數系統仍在用的 UUID 家族:第 1 版把時間戳與節點識別碼結合,第 3、5 版用 MD5 或 SHA-1 對名稱做雜湊,第 4 版則是純隨機資料。RFC 9562 保留這些,另外加入新的結構。

版本由什麼組成可排序典型用途
v160 位元時間戳、clock sequence、48 位元節點大致可以只留給舊系統
v3 / v5命名空間與名稱的 MD5 / SHA-1 雜湊不行由自然鍵產生決定性 ID
v4122 位元隨機不行主鍵、請求 ID、token
v6v1 欄位重排,時間戳在前可以既有 v1 部署的升級路徑
v748 位元 Unix 毫秒時間戳加上 74 位元可以新的資料庫主鍵
v8廠商自訂視實作實驗性結構

其中兩列帶有隱私警告。第 1 版與第 6 版可能內嵌由 MAC 位址衍生的節點識別碼,而規範明確指出 MAC 位址會帶來隱私風險,不應該放進 UUID。如果你還在產生 v1,這就是該搬走的理由。

02UUID v4 的運作方式

第 4 版是整個家族裡最單純的結構:取 122 位元隨機資料,再覆寫兩個小欄位,讓讀取者能分辨版本與 variant。

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
                ^    ^
                |    +-- variant:高位為 10
                +------- 版本:4

第三組開頭的 4 是版本 nibble,第四組的第一個字元被限制為 8、9、a 或 b,讓 variant 位元讀起來是 10,其餘每一個位元都是隨機的。

這留下了很大的餘裕。依生日界限,要出現 50% 碰撞機率大約需要 2^61.2 個值,也就是約 2.7 x 10^18 個 ID。若每秒產生十億個 UUID,大約要 86 年才會走到那個點。

隨機性的要求不是裝飾。只有當位元組來自密碼學安全的隨機來源時,第 4 版才真的不可猜測,而規範的隨機資料章節正是這樣要求。用 Math.random() 產生的字串雖然長得像 UUID,實際熵值遠低於這個格式的承諾。

03UUID v7 的運作方式

第 7 版用一部分隨機性換取排序性。它的前六個位元組是 big-endian 的 Unix epoch 毫秒數,其餘欄位沿用慣例的版本與 variant 規則:

欄位位元數內容
unix_ts_ms48自 1970-01-01 UTC 起的毫秒數,big-endian
ver40b0111,也就是 7
rand_a12隨機資料,或單調計數器的一部分
var20b10
rand_b62隨機資料,或計數器的其餘部分

扣掉版本與 variant 位元,同一個毫秒內還有 74 位元可以保證唯一。規範允許這些位元是純隨機資料,也可以是仔細播種的計數器,並提醒實作要確保批次產生的 UUID 保持單調。

48 位元時間戳到西元 10889 年才會用完,所以使用年限不是避開 v7 的理由。代價是這個 ID 不再不透明:拿到它的人就能讀出它是什麼時候建立的。接下來兩節分別處理這兩個後果。

04從 UUID v7 還原建立時間

因為時間戳就在最前面,不必任何函數庫就能解碼。取前 12 個十六進位字元,以 16 進位解析,得到的整數就是 Unix 毫秒。

01a10665-2c90-7250-9fa2-05b104d55e28
-------- ----
    |      |
    |      +-- 48 位元時間戳的後 8 個十六進位字元
    +--------- 前 16 位元

hex  01a106652c90
dec  1791108656272
date 2026-10-04T10:10:56.272Z

這些數字來自 2026-10-04 的本機實測,環境是 Node.js 24.14.1 搭配 uuid 套件 11.1.0。指令與解碼步驟如下:

node -e "const {v7}=require('uuid'); const id=v7();
  console.log(id, parseInt(id.replace(/-/g,'').slice(0,12),16))"

連續跑兩次,就會看到毫秒欄位在兩次呼叫之間變大。同一個行程連續產生的五個值,以純字串排序也正確,因為這個套件會播種一個單調計數器,在時鐘沒前進時遞增。

計數器是每個行程各自的。同一台機器上的兩個 worker,或時鐘略有差異的兩個容器,都可能產生出與寫入順序不一致的 v7 值。排序性是行為良好的產生器給的性質,不是格式本身保證的。

UUID v7 是你能讀的時間戳,不是你能信任的時間戳。

時鐘會漂移,NTP 可能往回跳,每個節點對「現在」也各有各的看法。請把內嵌時間當成除錯用的提示,而不是記錄建立時間的權威來源。

05排序性與索引區域性

把時間戳放前面換到的好處,會在資料庫索引裡出現。規範說得很明白:第 6 版與第 7 版可以被當成不透明的原始位元組排序,不需要解析;而且時間有序的 UUID 能享有索引區域性,實務上與隨機寫入的差距可達一個數量級以上。

這就是 v7 勝過 v4 當主鍵的理由。隨機鍵會插進 B-tree 的隨機位置,每一列新資料都可能碰到不同的葉節點,工作集會隨資料表成長;時間有序的鍵則插在尾端,也就是上一次寫入剛碰過的地方,熱頁面因此能維持精簡。

文字形式同樣可以排序,因為十六進位是固定寬度,而且最高位元組排在最前面。不必解碼就能得到時間順序:

01a10665-2c90-7250-9fa2-05b104d55e28   較早
01a10665-2c91-73bf-8070-fa5b3b1fd080   較晚

有三個實務上的注意事項。第一,v7 會把新鍵集中在索引尾端,這正是單節點寫入工作負載想要的形狀,也正是分散式系統可能變成熱分片的形狀;如果依 ID 前綴分片,v4 反而分散得更好。第二,時鐘往回跳可能產生排序錯誤的值。第三,UUID v4 與 v7 可以共存於同一欄位,但混在一起的欄位只有部分有序,所以不要以為排序歷史資料能得到有意義的結果。

如果你要的是人類可讀的可排序識別碼而不是 UUID,ULID 提供同樣的 48 位元毫秒前綴,但編碼方式不同。它不是可以直接替換的選項:ULID 是 26 個 base32 字元,沒有版本與 variant 欄位,所以 ULID 不是合法的 UUID,會驗證 UUID 結構的資料庫型別會直接拒絕。

06為什麼 UUID v7 不該當成祕密

不透明的識別碼不該告訴攻擊者任何事,而 v7 UUID 會:它洩漏這一列是哪一毫秒建立的,也洩漏記錄的到達順序。拿到兩個 ID,不必碰資料庫就能判斷哪一個先出現。

規範直接談到這件事:時間戳加上計數器會透露建立順序,因此如果 UUID 會被用在任何安全操作上,就應該改用 UUID v4。如果你要產生密碼重設連結、session token 或簽章 URL 的識別碼,v7 就是錯的版本。

UUID v4 也不是安全問題的完整答案。只要產生器使用安全隨機來源,它的 122 位元隨機值確實不可猜測,但它仍然是通用識別碼,而不是為特定用途打造的 token。用在 session 與一次性連結時,專門的 256 位元隨機 token 餘裕更大,也讓下一個讀程式的人更清楚意圖。

版本要依威脅模型選,不是依潮流選。

如果 ID 必須不可猜測,或不能洩漏建立順序,就用 v4。如果 ID 是資料庫主鍵、寫入模式主宰效能,就用 v7。兩者不能在兩個方向上都互換。

07到底該選哪一個?

多數系統最後會兩個都用,只是角色不同。下面這張表是取捨的簡短版本:

情境選擇原因
寫入密集資料表的新主鍵v7寫入區域性與時間排序
session token、重設連結、簽章 URLv4,或 256 位元隨機 token不能洩漏建立時間
跨服務的請求 IDv7日誌與追蹤會依時間排序
由名稱衍生的決定性 IDv5同樣輸入永遠得到同樣 UUID
既有資料表已在使用 v1v6 或 v7同一個位元結構家族下的遷移路徑
與既有隨機鍵共用欄位看情況混版只有部分有序

儲存方式的影響比版本選擇小。PostgreSQL 的原生 uuid 欄位兩種版本都存同樣的 16 位元組,文字形式也固定是含連字號的 36 個字元。真正改變的是寫入模式,以及這個值暴露了什麼。

08用瀏覽器產生 v4 與 v7

UrsaKit 的 UUID 產生器 兩種版本都能產生,而且不會把任何內容送到伺服器。

  1. 在 Version 選單選版本:UUID v4 (random) 或 UUID v7 (time-ordered),預設是 v4。
  2. 設定 How many,範圍 1 到 500,預設是 5。
  3. 依需要開關格式選項:Uppercase(預設開啟)、Keep hyphens、Braces {}、Quotes ""。
  4. 按下 Generate,再用 Copy all 或 Download 取走結果。

下面的輸出是 2026-10-04 以預設設定實際跑出來的值:

version 4, default options       A0ACD050-B919-44BC-BEB3-B55076F2F736
version 7, default options       01A10666-3D98-732B-9AFA-E9439D6029BE
version 7, no hyphens, lowercase 01a106663d98732b9afae9439d6029be

這三個都是那一次執行的真實結果。大寫與連字號預設開啟,把它們關掉只改變呈現方式,不改變底下的 128 位元。中間那個值解出來是 2026-10-04T10:12:06.168Z,比上面時間戳範例大約晚 70 秒。

除錯 v7 值時,把時間戳欄位貼進 時間戳轉換工具,就能看到 UTC 與本機時間;要手算十六進位,可以用 進位轉換工具。這個 ID 之後如果跟著 API payload 傳出去,JSON 驗證工具 是最便宜的一道檢查,確認整個 body 沒有被順手改壞。

09資料來源

常見問題

UUID v4 與 v7 差在哪裡?

UUID v4 用隨機資料填滿 122 位元,只在固定位置留下版本與 variant 位元。UUID v7 開頭是 48 位元 Unix 毫秒時間戳,其餘 74 位元放隨機值或計數器,所以 v7 會依建立時間排序。

UUID v7 可以排序嗎?

可以。時間戳佔的是最高位,加上文字形式是固定寬度的十六進位,所以 v7 不論以原始位元組或文字排序,都會依照建立順序。UUID v4 沒有時間戳,排序結果是隨機的。

可以從 UUID v7 還原建立時間嗎?

可以。取前 12 個十六進位字元,以 big-endian 解析成整數,就是 Unix 毫秒。本文實測的第一個值 01a10665-2c90-7250-9fa2-05b104d55e28 解出來是 2026-10-04T10:10:56.272Z。

主鍵該用 UUID v4 還是 v7?

持續寫入的鍵請用 v7。規範指出時間有序的 UUID 能享有資料庫索引區域性,實務上與隨機寫入的差距可達一個數量級以上。若值本身不能洩漏建立順序,就保留 v4。

UUID v7 跟 ULID 一樣嗎?

不一樣。兩者都以 48 位元毫秒時間戳開頭、都能依時間排序,但 ULID 用 26 個 Crockford base32 字元編碼,也沒有版本與 variant 欄位,所以不是合法的 UUID。UUID v7 維持 8-4-4-4-12 的十六進位格式與標準版本位元。

UUID 和 GUID 是同一個東西嗎?

是。GUID 是微軟對同一種 128 位元識別碼格式的稱呼,只要依標準結構與版本位元產生,GUID 就是 UUID。

可以把 UUID 當 session token 嗎?

UUID v7 不行,因為它會洩漏建立時間與順序。如果安全操作上非得用 UUID,規範說應該用 UUID v4,並以密碼學安全的隨機來源產生。實務上多數團隊仍偏好另外用 256 位元的隨機 token 來做 session。

返回文章列表

相關文章

相關工具