GUID 作為主鍵那些坑

聊聊用 UUID/GUID 作為主鍵那些坑

訪問欠友好的 UUID

我剛讀了一篇有關如何擴展數據庫的文章,作者建議將 UUID(類似於 GUID)用作數據庫表的主鍵(PK)。

使用 UUID 的優點

與自動遞增整數相比,將 UUID 用作主鍵的優點很多:

  • 適合大規模數據。當你把數據分片(例如一組客戶數據)存在多個數據庫時,使用 UUID 意味著 ID 在所有數據分片中都是唯一,而不僅僅是當前那個分片所在數據庫。這使得跨數據庫移動更為安全。在我的環境,所有數據庫分片都可以簡單合併到 Hadoop 集群中,不會有主鍵衝突的問題。

  • 在插入數據之前就可以知道 PK,這避免了查詢 DB 開銷,並簡化了事務邏輯,比如在使用該鍵作為其它表外鍵(FK)時,需要預先獲得這個 PK

  • UUID 不會洩露數據信息,因此在 URL中暴露會更安全。如果一個用戶 ID 是 12345678,很容易猜到還有用戶 12345677 和 1234569,這構成了攻擊因素。(請參見下面的更好的選擇)。

使用 UUID 的缺點

不直觀

很多人直接使用 UUID (類似像70E2E8DE-500E-4630-B3CB-166131D35C21 )作為字符串,例如 varchar(36) — 請不要這樣做!

你可能會說沒有人會這麼幹。

這個問題請三思 — 我曾接手過某些大公司中兩個非常大的數據庫案例,這恰恰是現實。除了 9 倍大小的開銷(int 的大小為 4 字節),字符串的排序速度也不如數值型字段快,因為它們依賴於排序規則。

在一家最初決定使用 Latin-1 字符集的公司中,情況就更麻煩。當我們需要將字符集轉換為 UTF-8 時,幾個複合鍵索引就存不下更改後的字符串。

UUID 的痛苦

不要低估字段太長無法口頭表達及記住的痛苦。

規劃擴展計劃

如果我們的目標是擴展,是真正地擴展,那麼首先要承認,int 在許多情況下不夠大,4 字節最大值限制在 20 億左右。我們很多數據庫都有超過 20 億筆交易。

因此在這些情況下需要使用 bigint,它佔用 8 個字節。還可以使用一些其他策略,諸如 PostgreSQL 和 SQL Server 等數據庫都有本機類型,可以使用 16 個字節存儲。

這時候,誰會在乎它的大小是 bigint 兩倍或 int 的四倍呢?畢竟也只是幾個字節,對不對?

規範化數據庫中主鍵的問題

如果你擁有一個規範的數據庫(就像我們目前所在的公司一樣),每個主鍵連接的外鍵都存在逐漸增加開銷。

不僅體現在磁盤上,還包括連接查詢和排序過程中,這些外鍵都需要存在於內存中。儘管內存越來越便宜,但無論是磁盤還是 RAM,它們都是有限的資源,也都不是免費的。

我們的數據庫有很多中間表,這些中間表是指向其他表外鍵的容器,尤其是在一對多關係中。帳戶具有多個卡號、地址、電話號碼,用戶名等。對於具有數十億用戶的一組表中的每個列,外鍵帶來額外空間會迅速增加。

對隨機字符串排序真的很麻煩

另一個問題是碎片化 — 由於 UUID 是隨機的,它們沒有自然順序,因此不能用於聚集索引(clustering index)。這就是為什麼 SQL Server 實現了newsequentialid,它適合在聚集索引中使用,並且可能是所有 UUID 主鍵的一個正確實現。

(見 https://msdn.microsoft.com/en-us/library/ms189786.aspx )

其他數據庫 PostgreSQL,MySQL 也可能有類似的解決方案。

主鍵不應該公開,即使是 UUID

根據定義,主鍵在其範圍內是唯一的。因此,用作用戶 ID 或在 URL 中當做唯一標識是很自然的事情。

但是別這樣做!我認為在任何公共場合暴露主鍵是一個很不好的做法。

前面提到,簡單的使用自動遞增 ID 的問題是,它很容易被猜測。攻擊爬蟲會不斷猜測,直到找到一個存在的為止(即使你轉向 UUID 爬蟲也可能會嘗試,但猜中的概率就變得非常的低)。

嘗試猜測一個 UUID 可能是愚蠢的事,但是微軟也警告過,引入 newsequentialid 來解決聚集索引問題,它使得它的數字更容易被猜測。

主鍵不需要修改(直到有人這樣做)

有令人信服的理由,不在任何公共語境暴露主鍵:比如你修改了數據表結構定義,所有外部引用被打破,想想遍地的“404 Page Not Found”頁面吧。

何時需要更改主鍵?碰巧的是,我們本週正在進行數據遷移,誰能在 2003 年公司成立之初就能預知現在存在 13 個海量 SQL Server 數據庫並且還在迅速增長?

不要說“從不用修改”,我已經經歷過好幾次。事先規劃簡單,但當你數據萬億級別時候,很難修復。

我目前公司的環境是一個很好的例子,說明了為什麼需要 UUID,以及它的成本高昂,為什麼對外暴露主鍵是一個問題。

我的內部系統是對外的

我管理 Hadoop 集群每晚都會從所有數據庫中接收數據。Hadoop 系統連接(綁定)到 SQL Server 數據庫,這也正常 — 畢竟我們是同一家公司。

儘管如此,為了消除多個數據庫中衝突的主鍵,我們通過連接兩個字段:用戶 ID(基於我們設計,它在所有庫中是唯一的),再加上業務表本身的 ID(存在多庫衝突),生成了一個偽主鍵。

通過這樣做,我們在多年的歷史用戶數據之間建立了緊密而有效的永久綁定。如果 RDBMS 中的一些主鍵發生了變化,我們數據也得更改這些主鍵,否則我們會在前後遇到一些可怕的情況。

兩者兼顧:內部整型,外部 UUID?

簡而言之,可採用兩者兼顧的方法,我已經在幾種不同場景下使用過這種解決方案。(不過注意:這不是一個好的解決方案,請參閱下面 Chris 評論)。

在內部,數據庫使用簡短高效的連續數字作為主鍵(int 或 bigint)。

然後添加一個 UUID 的字段(可以使用插入時的觸發器)。在數據庫本身的範圍內,可以使用常規的主鍵和外鍵來管理關係。

當需要將數據的引用公開給外部世界時,即使“外部”對應另外一個內部系統,它們也必須僅依賴 UUID。

這樣,如果確實必須更改內部主鍵,則可以確保它僅作用於一個數據庫。(注意:正如 Chris 觀察到的那樣,這樣也有缺陷)

我們在另一家公司使用此策略來存儲客戶數據,只是為了避免“可猜測的”問題。(注意:避免與防止不同,請參見下文)。

在另一種情況下,我們將生成一小段文本(例如在本文原文的地址中),從而使 URL 更加人性化。如果有重複項,則只需附加一個哈希值。

即使作為“第二主鍵”,直接以字符串形式使用 UUID 也是錯誤的:我更希望使用內置的數據庫機制,將其存儲為 8 字節整型數字。

使用整型是因為它們更高效有效。使用 UUID 的數據庫實現用於混淆的外部引用。

克里斯·羅素(Chris Russell)正確回應了本文的原始帖子,指出了兩個重要的邏輯錯誤。

首先,即使公開主鍵的替代品 UUID 也會暴露信息,這在使用 newsequentialid 函數時尤其如此 — 為了安全,請勿使用 UUID。

其次,如果內部的主鍵是整型值,仍然存在合併兩個數據庫的鍵衝突問題,除非所有鍵都加倍增大……在這種情況下,只使用 UUID。

因此,正確的做法是:使用 UUID 當做主鍵,並且永遠不要公開它們。內外影射的事情留給諸如友好 URL 處理之類的模塊,類似 Medium 那樣使用哈希值。

謝謝 Chris!(譯者:Chris 評論請參看原文鏈接中的評論部分)

感謝 Ruby 週刊(儘管我已經轉到 Scala,我仍然經常閱讀),Starr Horne 在 Honeybadger.io 上的博客, 以及 StackOverflow 的聯合創始人 Jeff Atwood 的 Coding Horror 博客上有趣而精巧的文章;感謝Stackoverflow 旗下網站 dba.stackexchange.com 上相關提問、MySqlserverTeam 的文章,還包括 theBuild.com 以及前文提到的 MSDN。

附註:為什麼寫這篇博客

通過寫這篇文章,我也學到了很多東西。

某個週日下午正在閱讀電子郵件,然後遇到了 Starr 的一篇有趣的帖子,我覺得他的方法可能會產生一些意料外的結果。因此,我通過 Google 搜索和學習了更多有關 UUID 的方法,並且改變了我對 UUID 使用的基本理解和態度。

在撰寫本文的一半時,我向公司的技術負責人發送了電子郵件,想知道我們是否考慮過我討論的這些話題。希望我們用過的方法沒有問題,避免計劃於本週發佈的代碼出現什麼意外。

請注意,我所做的這些都是出於完全自私的原因 :-)

希望你也喜歡它!

Image Credit:

http://unlockforus.blogspot.com/2008/03/advanced-how-to-creategenerate-new-guid.html

原文鏈接:

https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439

在你的項目中,是否有更好的 UUID 及主鍵做法?歡迎留言。

本文作者 Howon Lee,由高可用架構翻譯。技術原創及架構實踐文章,歡迎通過公眾號菜單「聯繫我們」進行投稿。

高可用架構

改變互聯網的構建方式


分享到:


相關文章: