[譯] 把 UUID 或者 GUID 作為主鍵?你得小心啦!

zaraguo發表於2017-06-30

把 UUID 或者 GUID 作為主鍵?你得小心啦!

沒有什麼會像 GUID 一樣表達“使用者友好”!

最近在閱讀時,一篇談論如何擴充套件資料庫的文章引起了我的關注 - 作者在文中建議大家使用 UUIDs(類似 GUIDs)作為資料庫表的主鍵。

UUIDs 的優點

下面列出了一些使用 UUID 作為主鍵比使用自增整數好的原因:

  1. 在擴充套件資料庫的時候,當你有多個資料庫包含同一段(片)資料時,比如一個顧客集,使用 UUID 意味著該 ID 在所有的資料庫中是唯一標識的,而不是僅僅本資料庫唯一。這保障了跨資料庫遷移資料的安全。又比如,我曾在專案中把多個資料庫分片合併到一個 Hadoop 叢集中,也沒有產生鍵的衝突。
  2. 在插入資料之前,你就能知道這個主鍵的值,這避免了一輪的資料查詢,並且簡化了事務的邏輯,即在你插入子記錄之前,因為需要使用這個主鍵作為一個外來鍵,你必須要知道這個主鍵的值。
  3. UUIDs 不會透露資料的資訊,因此被用在 URL 中也比自增整數更安全。比如,我是編號 12345678 號顧客,那麼人們就會猜測編號為 12345677 和 12345679 的顧客的存在,這就提供了一種攻擊向量。(但是後面我們會看到一個更好的替代品)

UUIDs 的缺點

不要太天真了

一個基礎的 UUID 大概是這個樣子的: 70E2E8DE-500E-4630-B3CB-166131D35C21,它將會被視為字串對待,比如 varchar(36) - 千萬不要這麼做!

你會說,“哼,才不會有人這麼做呢。”

我再三考慮了下 - 就我所接手的兩個大型企業級資料庫來看,他們確實是那麼實施的。除了 9 倍的多餘開銷外(比起 36 位元組,整數型別只佔了 4 位元組),字串在排序上也沒有數字快,因為它們依賴排序規則。

在一家公司還曾發生過十分糟糕的事情,一開始他們使用 Latin-1 字符集。當我們打算轉為 UTF-8 時,好幾個聯合索引因為太大而存不下。哦!

UUIDs 之殤

不要低估處理大到不能儲存和表達的值的惱人程度。

為實際的擴充套件做計劃

如果我們的目標是擴充套件,我是說真正的擴充套件。那麼首先讓我們意識到 int 型別在很多情況下是不夠大的。在大約 20 億(需要 4 位元組)的時候就溢位了。然而每個資料庫中我們都有遠超 20 億大小的資料存在。

因此,bigint 在某些時候才是我們真正需要的,它佔 8 個位元組。此外,還有其他多個策略可供選擇。像是 PostgreSQL 和 SQL Server 這些資料庫都有 16 位元組的原生型別。

誰會介意是否是 bigint 的兩倍或者 int 的四倍大小?這只是一點點位元組,對吧?

規範良好的資料庫中主鍵到處可見

如果你的資料庫有良好的規範,正如我現在所在的公司一樣,每一次將一個鍵用作外來鍵前會先進行評估。

不單單在磁碟上,在進行 join 和 sort 時這些 key 還需要載入到記憶體中。記憶體的確越來越便宜了,但是無論磁碟還是記憶體它們都是有限的,並且也都不是免費的。

我們的資料庫用大量的關係表來儲存外來鍵,尤其是在一對多的關係中。賬戶表內含有多個卡號,地址,電話號碼,使用者名稱等等。對於擁有數十億賬戶的一組表中的任意一列,外來鍵的空間開銷的增長都是十分快速的。

隨機數排序十分困難

另外一個問題就是碎片化 - 因為 UUIDs 是隨機的,他們沒有天然的生成順序因此不能夠被用於叢集。這就是為什麼 SQL Server 實現了一個 newsequentialid() 方法用於叢集化索引的使用,這可能就是將 UUIDs 作為主鍵使用的正確開啟方式了。其他的資料庫可能也有類似的解決方案,PostgreSQL,MySQL 肯定是有的,其他的可能有。

主鍵永遠不應該被暴露,甚至是 UUIDs

因為主鍵在其作用域內的唯一性,所以顯然可以用作使用者編號或者用在 URL 中來標誌唯一頁面或者記錄。

千萬不要!

下面我將闡明在公開環境中暴露主鍵是十分不好的這一觀點。

正如我上面所說過的,簡單的自增值的基本問題便是它們容易被猜到。殭屍網路可以利用這點不斷猜測直到找到真實值。(當然如果你使用 UUIDs,它們也可以進行暴力破解,只是猜中的機率將十分低)。

理論上說試圖猜中一個 UUID 可能是一件十分愚蠢的行為,然而 Microsoft 還是告誡我們不要使用 newsequentialid(),因為為了減少叢集問題,它其實較為容易猜測。

我曾以為我的鍵絕對不會變(直到它們變了)

不在公開環境使用主鍵還有一個無法反駁的原因:你一旦需要改變這個鍵值,那麼所有外在的引用就不可用了。想象一下 “404 頁面無法找到”的情形。

你什麼時候需要更改鍵值呢?真巧,我們這個星期在做資料遷移,因為在 2003 年一個公司剛起步的時候誰能想到我們現在會需要 13 個龐大的 SQL Server 資料庫並且依然在持續快速增長?

永遠不要說“絕不會”。我曾參與那次遷移專案,並且諸如此類的事情在我身上就發生過多次。與此相比,事先預防則更加簡單。當你置身數萬億的資料之中遷移將變得更加困難。

事實上,我現在公司的場景就是為什麼需要 UUIDs 的最好例子,以及為什麼 UUIDs 開銷巨大,為什麼在公開環境中暴露主鍵是一個問題。

我的內部系統是對外的

我管理的 Hadoop 基礎設施每晚都會接收到來自我們所有資料庫的資料。該 Hadoop 系統連線到我們的 SQL Server 資料庫,這沒什麼問題,因為這兩個同屬一家公司。

還有,為了避免多個資料庫間的序列化鍵衝突,我們通過關聯兩個值來生成了一個假的主鍵,跨資料庫唯一的客戶編號(主鍵),加上它們在表內的序列號。

通過這樣做我們在多年的歷史使用者資料之間建立了緊密且有效地永久聯絡。如果這些在關聯式資料庫管理系統中的主鍵發生了改變,我們與之相對應的鍵也要進行改變,否則將會產生令人恐懼的前後不一致。

如何兩全其美?內部引用用整型,外部引用用 UUIDs

有一個在多個不同場景下都有效的解決辦法,簡單來說就是,兩者都用。(請注意:這不是一個好方法 - 請看下面我記錄的 Chris 對原始博文回覆)

在內部,讓資料庫用小而有效、數值型的序列鍵來管理資料關係,int 或是 bigint 皆可。

然後增加一列用於存放 UUID(可以將其設計進插入的預處理操作裡)。在一個資料庫自身的範圍內,可以使用普通的主鍵和外來鍵來管理關係。

當需要暴露一個資料的引用到外部時,即使這裡的“外部”是另一個內部系統,它們也必須依賴 UUID。

這樣一來,如果你需要改變內部的主鍵,那麼你也可以確保它的影響範圍在一個資料庫內。(注意:正如 Chris 評論的,這點明顯錯了)

我們曾在另一個公司的客戶資料上採用了這個策略,正是為了避免主鍵“易被猜測”的問題。(注意:避免不同於阻止,詳見下文)。

另一種情況,我會生成了一“段”文字(例如像本篇一樣的博文)用於 URL 使其更加對使用者友好的。如果有衝突,那麼只需追加一段雜湊值。

即使作為“次級主鍵”(譯者注:這裡的次級主鍵指擁有主鍵特性用於外部引用的鍵),簡單地使用字串形式的 UUIDs 也是錯的:我推薦使用內建的資料庫機制生成 8 位元組整型值。

使用整型是因為它們是高效的。另外也可將資料庫實現的 UUIDs 用於無規律化外部引用,避免暴力破解。

Chris Russell 就原始博文的本節給予的迴應正確地指出了兩個重要的邏輯上的預警或者說是錯誤。第一點,即使用 UUID 代替真實的主鍵暴露在外,實際上也會披露很多資訊,特別是在用 newsequentialid 的時候 - 不用試圖用 UUIDs 來保證安全。第二點,如果所給的 schema 的關係在內部被整數鍵所管理,在合併兩個資料庫時你依然會有鍵衝突的問題,除非允許所有的鍵有兩個記錄存在...如果是這種情況的話,就使用 UUID。因此,在現實中,正確的解決方案可能是:你可以用 UUIDs 當做鍵,但是絕不要暴露他們。如何對內或是對外的事情最好還是留給像是 url 友好化處理的模組來負責,並且再(正如 Medium 所做的那樣)用一個雜湊值附加在尾部。感謝 Chris!

附言和感謝

感謝 Ruby Weekly(我始終在看,儘管我現在在用的是 Scala),來自 Honeybadger 公司的 Starr Horne 關於此觀點的優秀文章Jeff Atwood 在 Coding Horror 上發表的總是充滿幽默和智慧的文章,Stack Overflow 的聯合創始人,自然還有來自 Starkoverflow 的 dba.stackexchange.com 上的一個不錯的問題。當然還有一篇來自 MySqlserverTeam 的非常棒的文章,另一篇來自 theBuild.com 以及我此前給過連結的 MSDN。

後記:我為什麼寫這篇文章

我從寫這篇文章中學到了很多

事情開始於一個週日的下午, 我在看郵件。

然後我偶然看到一篇 Starr 寫的有趣的文章,這不禁讓我開始思考他的建議可能帶來一些意料之外的效果。因此我開始去 google 搜尋相關資料,而這拓寬了我對 UUIDs 的認識,並且改變了我對於如何使用它們的基本認知和態度。

寫作途中,我曾給公司的組長髮郵件詢問我們的資料庫設計是否考慮到了上面我所談論到的幾個觀點。但願我們做得很好,但是我想在本週計劃釋出的程式碼中我們已經避免掉了至少一個不可預計的意外。

寫下這篇文章純屬滿足私慾 :-)

但願你也能喜歡!

圖片來源


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章