分布式数据库主键 id 生成方案

分布式 id 生成策略

Posted by John Mactavish on July 14, 2021

对于传统的关系型数据库,数据以表的形式给出。表中经常有一个列或多列的组合,其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键,通过它可强制表的实体完整性。但并不是唯一的一列就能成为主键,主键还有其他要求。

主键必不为空,因此“电子邮箱地址”等可选字段不适合作为主键。 其次,主键最好不要变动以维护表的稳定,因而“用户名”等可修改的字段也不合适。总的来说,主键最好与业务无关以摆脱易变的业务的影响,所以一个很好的选择就是用机器自动生成的 id。

对于单个数据库,内置的自增 id 即可满足条件。但是在分布式场景下, 不同机器生成的 id 可能会重复,如果坚持需要全局唯一 id,则要另取他法。

数据库分块自增

不同机器的自增 id 重复的原因是它们的可生成 id 的集合重叠了, 通过一些方法将其分开来即可解决。例如, 我们为不同的数据库设置不同的起始点(auto_increment_offset), 这样还可以根据 id 大小直接判断出数据处在哪个数据库; 但是增加了维护成本,我们需要高度关注各个数据库的 id 增长状况, 以在某两块将要重合时选择新起始点。或者,我们可以为不同的数据库设置较大的自增步进(auto_increment_increment)并错开起始点,这样可保证它们的 id 永远正交。

方法一
A -> 1 2 3 4 ...
B -> 1001 1002 1003 1004 ...
C -> 2001 2002 2003 2004 ...

方法二
A -> 1 4 7 10 ...
B -> 2 5 8 11 ...
C -> 3 6 9 12 ...

但是当集群中数据库数量增加时,新的 id 分到哪一块呢? 无论如何,分块方法都要求我们预先估计数据库的数量,不利于以后的扩展。

Redis id 生成机

我们不妨将 id 生成的任务拿出来,让一个 Redis 服务器(或集群)单独来做。 具体来说,就是利用 Redisincr 原子性操作自增。 这样便轻松解决了 id 重复的问题。 但缺点是每次使用 id 都要向 Redis 进行网络请求,拖慢系统速度。

一个改进的方案是,每次请求 Redis 时不再仅获取一个 id, 而是获取一个 id 区间;区间中的 id 在本地自增生成。 这样便大幅减少了请求的次数。

UUID

一个想法是让各个数据库通过某种算法独立生成 id,算法本身保证 id 的唯一性。这一点 UUID(Universally Unique Identifier) 可以做到。 UUID 主要利用时间戳、计算机 MAC 地址、随机数等生成。 其标准形式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12。例如:550e8400-e29b-41d4-a716-446655440000。

但是 UUID 不保证有序性,这意味着新生成的 id 可能比上一个大, 也可能比它小。对于一些聚集主键类型的引擎(如 InnoDB)来说, 数据会按照主键进行物理排序;无序的 id 会导致新插入的数据不能直接 追加在源数据末尾,而要动用大量的 IO 操作移动数据,影响效率。 因此 UUID 不适合用作物理主键。比较适合的做法是把 UUID 作为逻辑主键, 物理主键依然使用自增 id。

Twitter 的 snowflake

snowflake 是专门设计的分布式 id 生成算法。 它可以生成一个 64 位整数,其结构如下:

1bit 41bit 10bit 12bit
符号位 时间戳(毫秒数) 机器 id 递增序列

12 bit 的递增序列是毫秒内的计数, 它意味着每个节点每毫秒(同一机器,同一时间截)可以 产生 4096 个 id。它同时可以保证整个 id 是趋势递增的。


最后附上 GitHub:https://github.com/gonearewe