- 原文地址: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、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。