雜記 › 技術筆記 › 自增主鍵被猜出訂單量?換 UUID 前先搞懂代價

自增主鍵被猜出訂單量?換 UUID 前先搞懂代價

去年幫一個做小程式商城的朋友看後台,訂單號是自增數字。有客戶在客服群裡抱怨:上一單 100231,這一單 100237,再翻幾個熟人的單號,一天出多少單基本擺出來了。老闆拿著截圖來問:能不能把對外那串號換成看不出東西的字元。本質不是加密,是不透明——編號只要單調遞增,差值就是一個擺在明面上的統計口徑。

換成隨機字串,最常見的是 UUID。我在他那個專案裡最後沒做全表遷移,只動了一欄對外欄位,原因在下面這三筆帳裡。

一、結構:128 位元裡有 6 位元是固定的

UUID 是 128 位元的識別碼,寫成 8-4-4-4-12 分組,共 32 個十六進位字元加 4 個連字號:

f3c1a072-8ad4-4b6e-91cf-0d5a2e6b7c10

128 位元全用滿,總空間是 2 的 128 次方,約 3.4×10 的 38 次方。但 v4 的隨機位元不是 128 位元,是 122 位元:從左往右數第 13 個字元(第三個分組的首字元)固定寫死為 4,版本位元佔 4 個二進位位元;第 17 個字元(第四個分組的首字元)只能取 8、9、a、b,因為變體位元的最高兩位固定為 10,佔 2 位元。128 減 4 減 2 才是隨機位元。

寫成「128 位元全隨機」是最常見的錯誤:空間被誇大成 64 倍(2 的 128 次方除以 2 的 122 次方正是 2 的 6 次方),後面的機率估算跟著偏。

二、碰撞機率只能按生日界算

要回答的不是空間大不大,而是產生 n 個之後出現一對重複的機率有多大。口徑用生日界。

N = 2^122 = 5316911983139663491615228241121378304
p = 1 - exp( -n*(n-1) / (2*N) )
n 遠小於 N 時:p ≈ n*n / (2*N)

這裡的 N 請用整數運算取 pow(2,122),浮點會把尾數改掉;上面那串 37 位數就是精確值。反解 p = 0.5,需要的產生量約等於 1.1774 乘以 N 的平方根,也就是 1.1774 乘以 2 的 61 次方(2305843009213693952),得 2714899559048203264 個,量級 2.7×10 的 18 次方。流傳的說法把它寫成「幾十億」,差了十個數量級。

再用極端口徑核對:每秒 10 億個跑滿 100 年(一年按 365 天算 31536000 秒)是 3153600000000000000 個,比閾值還多 16%,同一公式代進去機率約 61%。所以「跑很多年也幾乎不會重複」方向是反的,它跑到的是拋硬幣,不是零。

我實際遇到的量級在另一頭:一次寫入 1000 萬筆,n 是 10 的 7 次方,p 約 9.4×10 的負 24 次方,當作不會發生是可以的。但口徑要寫清:10 的 7 次方這一檔安全,10 的 18 次方那一檔另說,兩檔不該塞進同一句話。

三、換成隨機主鍵要付的三筆帳

第一筆是索引寫入。自增主鍵永遠落在 B+ 樹最右邊;隨機主鍵的落點由那 122 位元決定,頁分裂更頻繁、快取命中率下降。共識只能說到這兒:隨機主鍵通常比自增慢、寫放大更多。誰給出「慢 3 倍」「索引大 4 倍」,讓他把資料表結構、資料庫版本和壓力測試資料一起拿出來。

第二筆是位元組。v4 寫成字串是 36 位元組,存成 BINARY(16) 是 16 位元組。按他那張 2 億行的訂單表算:字串主鍵 7200000000 位元組,二進位主鍵 3200000000 位元組,光一欄就差 4 GB。InnoDB 還把主鍵值複製進每個二級索引的葉節點,4 個二級索引就是主鍵落 5 份:36 位元組約 36 GB,16 位元組約 16 GB。欄位若宣告成 utf8mb4 的 CHAR(36),定長按每字元最多 4 位元組計,2 億行是 28.8 GB。

第三筆是人和系統的摩擦。36 個字元沒法口頭報修,日誌裡兩三條就刷滿螢幕;隨機字串不帶時間含義,「最近 10 單」要靠 created_at 排,別指望主鍵順序可讀。

項目 UUID v4(隨機) UUID v1(時間戳) 自增 ID
位元從哪來 122 位元隨機,6 位元固定 時間戳加節點標識 表內計數器
主鍵寫入形態 落點隨機,頁分裂多 前段隨時間推進 順序追加
單值位元組 36 字元 / 二進位 16 同上 4 或 8 位元組
陌生人能讀出什麼 讀不出量級與時間 可能讀出產生時間 做減法就知道出單量
順序可用嗎 不可讀 跨機不保證單調 嚴格遞增
本站工具 直接產生 前端模擬,無真實 MAC 不適用

四、工具只是這條鏈上的一環

決定用 v4 之後先要寫入測試資料。我日常用 UUID 產生器:產生 v4(隨機)或 v1(時間戳),一次批次 1 到 100 個,格式選項是大寫或小寫、帶不帶連字號、要不要大括號,單筆複製或全部複製都行;要 5000 行就取 50 次。對方只認大括號大寫的 GUID 寫法時,格式那三項比版本更有用。

100 行 ID 貼進 文字行操作,給每行統一加前後綴,拼成 INSERT 語句的每一行。麻煩從來不在產生,在產生之後那幾十次字串處理。

也拿它做過冪等鍵:用戶端為同一次提交重複使用同一個鍵,伺服器端記下見過的鍵,重複提交就不再執行第二次。它防的是重複提交,不解決併發正確性——兩個請求真的同時改同一行,還得靠鎖或版本欄位。

關於 v1 有一句必須交代:本站工具的 v1 是純前端模擬,不讀取真實 MAC 位址,因此不具備真實 MAC 衍生的可追溯性,和套件裡拿網卡位址當節點標識的 v1 不是一回事。要追到產生它的那台機器,請用套件實作。

五、這幾個問題我被問過最多

Q:隨機位元到底是 122 還是 128?

122。版本位元 4 位元加變體位元 2 位元固定。記成 128 會把空間誇大約 64 倍,50% 閾值跟著錯一檔。

Q:想按時間排序,換成 v1 就夠了嗎?

前段確實隨時間推進,但本站 v1 是前端模擬、不含真實 MAC,也不保證跨行程單調。真要有序,常見做法是在 ID 前面自己拼時間分段,別把期望放在隨機字串的順序上。

Q:能直接拿 UUID 當密碼、邀請碼或金鑰嗎?

不建議。那 122 位元隨機是為標識唯一服務的,格式寫死、產生來源不承諾密碼學強度,還常整條進日誌。要密碼就用 隨機密碼產生,長度和字元集(大小寫、數字、符號)由你定,熵能明確算出來。產生來源用途不同:一個給人驗證身分,一個給機器對帳。

Q:已經上線的自增主鍵要不要遷成 UUID?

先看這個 ID 會不會被陌生人看到。只在內部關聯用的表我不動;出現在介面、URL、匯出檔案裡的,加一欄對外不透明編號、內部仍留自增,索引重建和外鍵連鎖就只壓在一欄上。

Q:用戶端產生的 UUID 會不會被偽造?

會,而且它本來就不證明任何事:它只保證這一串大概率獨一份,不保證請求是誰發的、內容改沒改。猜不到不等於可信,驗證身分要靠簽章。

回到開頭那筆單子:朋友的資料表沒改主鍵,內部 id 仍自增,對外多一欄 order_no 存 v4,介面和匯出只露它。不透明只需要在暴露面上成立,而索引、位元組、摩擦這三筆帳在一欄上的代價遠小於在 2 億行主鍵上。要不要換,看你的 ID 會不會被陌生人看到;要換,就先接受隨機性的寫入代價,再從二進位欄、索引份數、排序欄位這三處把它省回來。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22