- 原文地址:UUID or GUID as Primary Keys? Be Careful!
- 原文做者:Tom Harrison Jr
- 譯文出自:掘金翻譯計劃
- 譯者:zaraguo
- 校對者:canonxu yifili09
沒有什麼會像 GUID 同樣表達「用戶友好」!html
最近在閱讀時,一篇談論如何擴展數據庫的文章引發了個人關注 - 做者在文中建議你們使用 UUIDs(相似 GUIDs)做爲數據庫表的主鍵。前端
下面列出了一些使用 UUID 做爲主鍵比使用自增整數好的緣由:mysql
一個基礎的 UUID 大概是這個樣子的: 70E2E8DE-500E-4630-B3CB-166131D35C21
,它將會被視爲字符串對待,好比 varchar(36)
- 千萬不要這麼作!react
你會說,「哼,纔不會有人這麼作呢。」android
我再三考慮了下 - 就我所接手的兩個大型企業級數據庫來看,他們確實是那麼實施的。除了 9 倍的多餘開銷外(比起 36 字節,整數類型只佔了 4 字節),字符串在排序上也沒有數字快,由於它們依賴排序規則。ios
在一家公司還曾發生過十分糟糕的事情,一開始他們使用 Latin-1 字符集。當咱們打算轉爲 UTF-8 時,好幾個聯合索引由於太大而存不下。哦!git
不要低估處理大到不能存儲和表達的值的惱人程度。github
若是咱們的目標是擴展,我是說真正的擴展。那麼首先讓咱們意識到 int
類型在不少狀況下是不夠大的。在大約 20 億(須要 4 字節)的時候就溢出了。然而每一個數據庫中咱們都有遠超 20 億大小的數據存在。sql
所以,bigint
在某些時候纔是咱們真正須要的,它佔 8 個字節。此外,還有其餘多個策略可供選擇。像是 PostgreSQL 和 SQL Server 這些數據庫都有 16 字節的原生類型。數據庫
誰會介意是不是 bigint
的兩倍或者 int
的四倍大小?這只是一點點字節,對吧?
若是你的數據庫有良好的規範,正如我如今所在的公司同樣,每一次將一個鍵用做外鍵前會先進行評估。
不僅僅在磁盤上,在進行 join 和 sort 時這些 key 還須要載入到內存中。內存的確愈來愈便宜了,可是不管磁盤仍是內存它們都是有限的,而且也都不是免費的。
咱們的數據庫用大量的關係表來存儲外鍵,尤爲是在一對多的關係中。帳戶表內含有多個卡號,地址,電話號碼,用戶名等等。對於擁有數十億帳戶的一組表中的任意一列,外鍵的空間開銷的增加都是十分快速的。
另一個問題就是碎片化 - 由於 UUIDs 是隨機的,他們沒有自然的生成順序所以不可以被用於集羣。這就是爲何 SQL Server 實現了一個 newsequentialid()
方法用於集羣化索引的使用,這可能就是將 UUIDs 做爲主鍵使用的正確打開方式了。其餘的數據庫可能也有相似的解決方案,PostgreSQL,MySQL 確定是有的,其餘的可能有。
由於主鍵在其做用域內的惟一性,因此顯然能夠用做用戶編號或者用在 URL 中來標誌惟一頁面或者記錄。
千萬不要!
下面我將闡明在公開環境中暴露主鍵是十分很差的這一觀點。
正如我上面所說過的,簡單的自增值的基本問題即是它們容易被猜到。僵屍網絡能夠利用這點不斷猜想直到找到真實值。(固然若是你使用 UUIDs,它們也能夠進行暴力破解,只是猜中的概率將十分低)。
理論上說試圖猜中一個 UUID 多是一件十分愚蠢的行爲,然而 Microsoft 仍是告誡咱們不要使用 newsequentialid()
,由於爲了減小集羣問題,它其實較爲容易猜想。
不在公開環境使用主鍵還有一個沒法反駁的緣由:你一旦須要改變這個鍵值,那麼全部外在的引用就不可用了。想象一下 「404 頁面沒法找到」的情形。
你何時須要更改鍵值呢?真巧,咱們這個星期在作數據遷移,由於在 2003 年一個公司剛起步的時候誰能想到咱們如今會須要 13 個龐大的 SQL Server 數據庫而且依然在持續快速增加?
永遠不要說「毫不會」。我曾參與那次遷移項目,而且諸如此類的事情在我身上就發生過屢次。與此相比,事先預防則更加簡單。當你置身數萬億的數據之中遷移將變得更加困難。
事實上,我如今公司的場景就是爲何須要 UUIDs 的最好例子,以及爲何 UUIDs 開銷巨大,爲何在公開環境中暴露主鍵是一個問題。
我管理的 Hadoop 基礎設施每晚都會接收到來自咱們全部數據庫的數據。該 Hadoop 系統鏈接到咱們的 SQL Server 數據庫,這沒什麼問題,由於這兩個同屬一家公司。
還有,爲了不多個數據庫間的序列化鍵衝突,咱們經過關聯兩個值來生成了一個假的主鍵,跨數據庫惟一的客戶編號(主鍵),加上它們在表內的序列號。
經過這樣作咱們在多年的歷史用戶數據之間創建了緊密且有效地永久聯繫。若是這些在關係數據庫管理系統中的主鍵發生了改變,咱們與之相對應的鍵也要進行改變,不然將會產生使人恐懼的先後不一致。
有一個在多個不一樣場景下都有效的解決辦法,簡單來講就是,二者都用。(請注意:這不是一個好方法 - 請看下面我記錄的 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 的認識,而且改變了我對於如何使用它們的基本認知和態度。
寫做途中,我曾給公司的組長髮郵件詢問咱們的數據庫設計是否考慮到了上面我所談論到的幾個觀點。希望咱們作得很好,可是我想在本週計劃發佈的代碼中咱們已經避免掉了至少一個不可預計的意外。
寫下這篇文章純屬知足私慾 :-)
希望你也能喜歡!
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。