[译] 把 UUID 或者 GUID 作为主键?你得小心啦!

把 UUID 或者 GUID 作为主键?你得小心啦!

[译] 把 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 的认识, 并且 改变了我对于如何使用它们的基本认知和态度。

写作途中,我曾给公司的组长发邮件询问我们的数据库设计是否考虑到了上面我所谈论到的几个观点。但愿我们做得很好,但是我想在本周计划发布的代码中我们已经避免掉了至少一个不可预计的意外。

写下这篇文章纯属满足私欲 🙂

0

发表评论