Skip to main content

参考:唯一 ID 生成

一份关于无需协调地生成唯一 ID 的参考——自增、hash、UUID、雪花风格与分段分配——以及每种场景该选哪一种、为什么 check-then-write 必然有竞态。

它是什么

几乎每个系统都需要一种标识符,能不必去问某一个全局数据库就生成出来:一个短链接码、一个消息 id、一个订单 id。设计上的问题是如何做到不需要协调的唯一性,以及你还需要三个属性中的哪几个:id 能否按时间排序、是否够小(例如一个 64 位整数)、是否可被猜到(这是安全问题)。

Important

核心要点: 任何”先生成、检查是否冲突、再写入”的方案都有 check-then-write 的竞态:两台服务器都可能发现没有冲突、然后都写入。要从构造上消除冲突(计数器,或者时间 + 机器 + 序列号),而不是靠碰运气。

何时需要它

它出现在短链接服务、聊天、订单和信息流里。信号在于:先用具体的并发故障否定较弱的选项,再做选择。

Note

面试信号: 先走一遍 hash 和随机 id,用具体的并发故障逐一否定,然后落在分段分配的计数器或雪花风格的 id 上,这就是对的做法。直接跳到”我会用一个计数器”而不说其他方案为什么不行,就错过了这道题的重点。

各种选项

方案做法可按时间排序大小冲突需要协调
自增(数据库序列)一个数据库里的一个计数器是64 位无需要:单一瓶颈和单点故障
内容 hashMD5 或 SHA,截断并编码否自选有,且存在 check-then-write 竞态不需要
随机(UUID v4)122 位随机数否128 位可忽略不需要
时间有序的随机(ULID、UUID v7)时间戳前缀加随机数是128 位可忽略不需要
雪花风格时间戳、机器 id、序列号打包成 64 位是64 位无,由构造保证一次性的机器 id 分配
分段分配中央分配器给每台服务器发一个计数器值的区块大致是64 位无很少:每个区块一次

Hash 和随机的短码以同一种方式失败。一个 7 个字符的 base62 码有 62⁷ ≈ 3.5 万亿个取值,所以在已存 10 亿个时,一个随机码每次尝试冲突的概率约 0.03%。听起来可以忽略,但检查冲突(“这个码被占用了吗?“)然后写入是两步,两台服务器同时做时都会通过检查。对长 URL 做 hash 有同样的竞态,更糟的是内容上的冲突,所以你必须探测并重试。两者都把一个唯一性问题变成了并发问题。

分段分配(计数器区块)

每台应用服务器向一个中央分配器(一个用 SELECT ... FOR UPDATE 的数据库行、ZooKeeper,或一个小服务)请求一个包含,比如,1,000 个值的区块,然后在本地发放,不需要任何协调。区块用完时再请求一个。把整数用 base62 编码得到短码。分配器每 1,000 个 id 才被触碰一次,所以一个分配器就能支撑巨大的写入速率,而 id 由构造保证唯一。如果某台服务器死了,它那个区块里没用完的部分直接被跳过。当 id 只需要唯一时,乱序的 id 没有问题。

雪花风格的 id

由三个字段构成的 64 位 id:

| 41 位:自自定义纪元以来的毫秒数 | 10 位:机器 id | 12 位:序列号 |

算例。 41 位毫秒数大约是自定义纪元起 69 年;10 位允许 1,024 台机器;12 位允许每台机器每毫秒 4,096 个 id,大约每台机器每秒 400 万个。因为时间戳在高位,id 按时间排序,这给聊天和信息流提供了免费的排序 key,并让数据库插入大致追加在索引的右边缘(对 B-tree 有利)。

要点出的故障模式:时钟回拨(一次 NTP 调整)可能产生重复或乱序的 id,所以生成器会拒绝发放 id,直到时钟追上,或者沿用上一个时间戳;机器 id 冲突(两台机器配置了同一个 id)会产生重复,所以机器 id 要来自一个注册表;序列号每毫秒重置一次。

时间有序的随机 id(ULID、UUID v7)在高位放一个时间戳,其后是随机数,既可排序,又不需要机器 id 注册表,代价是 128 位。

Warning

生产实践: 可排序的 id 会泄露信息。时间戳前缀暴露了创建时间,顺序的 id 暴露了数量(“订单 100,482”告诉竞争对手你接了多少订单)。如果 id 会对外暴露,就加一个随机成分,或把内部 id 映射成不透明的公开 id。

如何选择

情形选择原因
短链接码分段分配的计数器,base62由构造保证唯一,分配器负载极小
必须在一个会话内排序的消息或事件 id雪花风格或 ULID可按时间排序且唯一
单个数据库、规模不大自增最简单
来自许多独立写者的 id,没有注册表UUID v4,如需可排序则用 v7不需要协调;冲突可忽略
不能被猜到的公开标识符随机,或不透明的映射无法被枚举
幂等键客户端生成的 UUID每个逻辑操作唯一

Caution

常见错误: 声称 hash 或随机 id “没有冲突”。它只是冲突不太可能,而 check-then-write 这一步又把竞态带了回来。如果唯一性是一个正确性属性(短链接码、主键),就用唯一约束来强制,或从构造上保证它。

相关