来自公众号:真没什么逻辑
作者Draveness
链接:https://draveness.me/whys-the-design-meaningless-identifier/
为什么这么设计(Why's THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。
标识符 ID 是我们作为软件工程师再熟悉不过的字段和概念了,我们经常打交道的 MySQL 就经常使用 ID 作为主键,ID 在软件工程甚至生活中都是一个非常常见的概念,我们为什么总是需要在业务或者系统中引入『无意义』的 ID 呢,在这里先来看两个有意义的 ID:
日常生活中使用的 18 位身份证号;
支付票据上面的
20191002XXXXXXX
;
上述的两个 ID 是否会有重复的可能?这对于今天想要分析和讨论的事情密切相关,在这篇文章中作者就会分析『为什么总是需要无意义的 ID』。
概述
我们首先需要解决的问题是 ID 到底是什么,ID 一般会被认为是 identifier 的缩写,在 Wikipedia 上我们能够找到这样的定义:
An identifier is a name that identifies (that is, labels the identity of) either a unique object or a unique class of objects, where the "object" or class may be an idea, physical [countable] object (or class thereof), or physical [noncountable] substance (or class thereof). The abbreviation ID often refers to identity, identification (the process of identifying), or an identifier (that is, an instance of identification). An identifier may be a word, number, letter, symbol, or any combination of those.
标识符(identifier)就是一个可以唯一识别一个对象或者物体的名称,被识别的对象可能是一些想法、物理上可数的对象或者物理上的不可数物质。它的前缀 ID 经常被用来表示身份、鉴定过程或者标识符,其中的标识符可能是一个单词、数字、字母、符号或者上述元素的任意组合。
在标识符的定义中我们需要特别注意的是『唯一』这个词,这个词是其定义中最关键的信息,标识符一定能够帮助我们识别唯一一个的对象或者物体,如果它不能实现这个作用,就不是标识符。唯一这个词帮助我们确定了标识符的特性,也为我们后面的分析过程铺平了道路。
设计
在这一节中我们将开始分析为什么很多业务或者场景中都需要一个唯一 ID,例如:消息队列、TCP 通信等场景,我们可以将这一问题归结到两个原因上:
需要通过唯一的标识符对数据或者事件进行识别或者去重;
只有无意义的标识符才会绝对唯一的,任何携带其他信息的标识都可能重复;
唯一性
消息队列往往需要对外保证服务质量,可能需要提供包括最多一次、最少一次和正好一次在内的服务质量,由于网络可能存在超时等不确定性,当我们想要实现正好一次时,就一定需要一种机制能够在接收方识别发送方发出的重复消息,在这时就需要使用唯一的标识符来解决这个问题:
我们在之前的系列文章 为什么 TCP 建立连接需要三次握手 提到的 TCP 连接中的序列号也是一个唯一的标识符,它能够帮助我们判断对数据进行去重,保证应用层的协议不会收到异常的数据包,这些场景都需要用到标识符的唯一性,唯一性为我们带来的就是精确识别对象的能力。
在与网络相关的场景下,使用唯一 ID 的例子非常普遍,假如我们想通过支付宝或者微信的 API 向其他人发起一笔转账,如果这次请求发生了超时,那么我的这笔转账请求到底有没有被处理呢?
当前的节点对于这笔转账请求的结果是不知道的,如果这时重新请求可能会发生二次转账这类严重的问题,但是如果不重新请求,转账可能没有生效,这时如果我们引入一个无意义的 ID 来帮助接收方识别请求的唯一性就能很好地解决这个问题:
如果接收方已经成功处理 ID 对应的请求,那么就直接返回;
如果接收方没有处理 ID 对应的请求,就正常进行处理;
为了保证请求的唯一性,根据业务对于唯一性要求的强弱,我们需要在接收方对 ID 进行存储,可以在内存中,也可以在数据库中,最重要的是唯一的 ID 为接收方提供了判断重复的重要依据。
除了在不稳定的网络中,数据库也包含 ID 标识符这一概念,我们在数据库中往往叫做主键,它在一般情况下都是一个递增的唯一整数,绝大多数的表都会使用 ID 作为表的主键来保证数据的唯一性,当我们想要对数据进行增删改查等操作时,使用主键 ID 查询数据也是性能最优并且最不容易出现问题的做法。
无意义
无意义的意思其实就是 ID 中不应该包含任何与具体场景或者业务相关的内容,包含这些内容并不是不可以,只是一旦出现这些内容,要么 ID 重复的可能性会增加,这很可能对我们的业务逻辑造成比较严重的影响,以我们的身份证号为例,它的 18 位数字(或符号)大多都是有意义的。
这 18 位数字中的前 6 位表示的是地区,也就是省份、城市和区县,随后的 8 位表示的是出生年月日,接下来的 3 位才同时表示 ID 和性别,最后 1 位用于做校验码防止出现身份证号输错的情况。用上述图中的黄色部分中有一半的数字是用来表示出生的男性,另一半表示出生的女性,所以如果同一个地区的同一天,同时出生了 501 位男性或者女性就会导致潜在的重复问题。
上面谈到的问题其实也是我们在各种业务场景中经常能够遇到的问题,18 位的数字中真正用于表示序列的 ID 其实只有 1000 的一半,如果 18 位数都是无意义的,那它们可以表示 10 亿亿个人,但是一旦在 ID 中引入了业务上的具体信息,就增加了冲突的可能性。
业务记录上主键的长度往往都是固定的,大多数业务的主键都会使用整数,它的上限一般就是 2^64
,如果这些位数都用来表示记录的 ID,那么在有生之年基本上是不可能被使用完的,但是一旦我们将业务信息加入 ID,就会让原本无意义的 ID 变得有意义从而影响它的唯一性。
另一个比较类似的例子其实是分布式的 ID 生成器,Snowflake 算法会为 64 个比特的整数赋予不同的信息:
范围 | 长度 | 作用 |
---|---|---|
0-0 | 1 | 不使用 |
1-41 | 41 | 毫秒级时间戳 |
42-46 | 5 | 数据中心标识符 |
47-51 | 5 | 机器标识符 |
52-63 | 12 | 序列号 |
从这个设计来看,我们的假设其实是一台机器上一毫秒最多只能生成 4096 个 ID,一旦超过了这个这个数量就有可能导致 ID 冲突或者乱序,从而失去其唯一性;这个算法中涉及的时间戳、数据中心标识符、机器标识符都没有办法解决唯一性的问题,哪怕这三者完全相等,最终还是有冲突的可能,我们仍然需要使用无其他意义的序列号来保证 ID 的唯一。
总结
其实不难看出,使用无意义 ID 的主要目的就是利用它的唯一性保证对象的标识符不会发生冲突,无意义 ID 的唯一作用就是保证唯一性,这能帮助我们避免业务字段可能存在潜在冲突的可能,这也提示我们想要使用联合字段构成主键时一定要深思熟虑。
如果我们想要在具有唯一性的标识符中加入业务信息,一定要注意这可能会减少用于保证唯一性的『空间』,当然对于一个足够大的空间来说,这其实并没有什么问题;但是类型为 int64
的 ID 中加入业务数据还是需要仔细思考可扩展性以及预留的信息是否足够业务的发展。
到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:
软件工程还有哪些场景利用了 ID 的唯一性?
在日常生活中除了身份证号之外,还有哪些 ID 也有比较高的冲突可能性?
如果对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的原因,可以在博客下面留言,作者会及时回复本文相关的疑问并选择其中合适的主题作为后续的内容。