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

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

写下这篇文章纯属满足私欲 :-)

但愿你也能喜欢!





原文发布时间为:2017年6月30日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-01 03:38:31

[译] 把 UUID 或者 GUID 作为主键?你得小心啦!的相关文章

序列作为主键使用的原理、优缺点讨论

这几天和同事一直在讨论关于表设计中主键选择的问题,用sequence作为主键究竟有什么好处,又有什么缺点,尤其是有些事务场景上下文需要用到创建的序列值,如何用?其实我想说的是,可能只是一个很简单的概念,可能深入理解,还是有很多未知的知识,当然也就可能会有一些容易忽略但又可能很关键的坑,只有碰了才知道... 以下是总结摘要,如有疏漏,还请过路的各位大侠赐教.   1.首先说下seq.nextval主要有以下两种使用场景:(1). 如果一个事务中只是INSERT时需要序列,其他地方不会需要这个序列,

在Activiti中使用UUID作为主键生成策略

1. 默认的主键生成策略 了解过Activiit表中数据的同学可能知道记录的主键ID是用自增的生成策略,这样的生成策略有两个优点: 有顺序:所有引擎的表在插入新记录时全部使用同一个ID生成器 便于记忆:因为是自增的所以有一定的顺序,便于记忆:例如业务人员让管理员删除一条数据(ID为5位左右的长度),管理员只要看一眼就可以记住 当然也有缺点: 随着时间的推移或者数据量非常大自增的ID生成策略的"便于记忆"优势也就不存在了,因为ID的位数会逐步增加(举个例子,我们公司做的一个小系统,用户量

数据库中使用自增量字段与Guid字段作主键的性能对比

1.概述: 在我们的数据库设计中,数据库的主键是必不可少的,主键的设计对整个数据 库的设计影响很大.我就对自动增量字段与Guid字段的性能作一下对比,欢迎大家 讨论. 2.简介: 1.自增量字段 自增量字段每次都会按顺序递增,可以保证在一个表里的主键不重复.除非超 出了自增字段类型的最大值并从头递增,但这几乎不可能.使用自增量字段来做 主键是非常简单的,一般只需在建表时声明自增属性即可. 自增量的值都是需要在系统中维护一个全局的数据值,每次插入数据时即对此 次值进行增量取值.当在当量产生唯一标识

MySQL查询优化:用子查询代替非主键连接查询实例介绍_Mysql

一对多的两张表,一般是一张表的外键关联到另一个表的主键.但也有不一般的情况,也就是两个表并非通过其中一个表的主键关联. 例如: 复制代码 代码如下: create table t_team ( tid int primary key, tname varchar(100) ); create table t_people ( pid int primary key, pname varchar(100), team_name varchar(100) ); team表和people表是一对多的关

关于Int自增字段和GUID字段的性能测试。只有测试,没有分析,呵呵

      最近有两篇关于GUID和Int自增的文章,我是一直使用Int自增的,不习惯使用GUID,感觉GUID很麻烦,用着不方便,性能也比不上Int自增.但是同时我也知道,二者在性能上孰优孰劣,只是感觉和猜测,并没有做测试!我是只相信测试,不相信分析.推断的.可能是由于我一直都没有系统的学习过的原因吧,高分析总是迷迷糊糊,模棱两可的.所以我更详细测试的结果.       一直想做一下这方面的测试来着,但是比较懒了,一直没有测试,看到了两片博文,勾起了我的兴趣,呵呵,测试一回吧. 一. 测试环境

mysql读写性能测试

用mysqlslap进行mysql压力测试 mysqlslap官方文档   mysqlslap菜鸟译文 概述和测试环境 压测的目的是为了尽量模拟真实情况.测试的表都是由10个int型字段和10个字符串型字段组成.每个测试项目都测试了myisam和innodb两个引擎.测试的方法都是用两个线程并发,一共跑10000个请求. 压测的机器用的是自己的笔记本.CPU是Intel(R) Core(TM)2 Duo CPU     P8400, 4G内存, SATA硬盘. mysql服务器端和压测的客户端跑

Javascript生成全局唯一标识符(GUID,UUID)的方法_javascript技巧

全局唯一标识符(GUID,Globally Unique Identifier)也称作 UUID(Universally Unique IDentifier) . GUID是一种由算法生成的二进制长度为128位的数字标识符.GUID 的格式为"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",其中的 x 是 0-9 或 a-f 范围内的一个32位十六进制数.在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID. GUID 的总数达到了2^128(3

sqlserver数据库主键的生成方式小结(sqlserver,mysql)_MsSql

主键的生成方式主要有三种: 一. 数据库自动生成 二. GUID 三. 开发创建 严格讲这三种产生方式有一定的交叉点,其定位方式将在下面进行讲解. 第一种方式,主要将其定位在自增长的标识种子:可以设置起始数值,及增长步长.其优点在于使用时完全将并发任务交于数据库引擎管理,你不用担心存在多用户使用的时候会产生两个相同的ID的情况.其缺点也在于此,多数的数据库不提供直接获取标识ID的方式,对于开发人员来说产生ID的方式是透明的,开发人员几乎无法干预此项.对于数据的迁移也不是很方便. 由于存在上面的利

sqlserver数据库主键的生成方式小结(sqlserver,mysql)

主键的生成方式主要有三种: 一. 数据库自动生成 二. GUID 三. 开发创建 严格讲这三种产生方式有一定的交叉点,其定位方式将在下面进行讲解. 第一种方式,主要将其定位在自增长的标识种子:可以设置起始数值,及增长步长.其优点在于使用时完全将并发任务交于数据库引擎管理,你不用担心存在多用户使用的时候会产生两个相同的ID的情况.其缺点也在于此,多数的数据库不提供直接获取标识ID的方式,对于开发人员来说产生ID的方式是透明的,开发人员几乎无法干预此项.对于数据的迁移也不是很方便. 由于存在上面的利