对于传统的关系型数据库,数据以表的形式给出。表中经常有一个列或多列的组合,其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键,通过它可强制表的实体完整性。但并不是唯一的一列就能成为主键,主键还有其他要求。
主键必不为空,因此“电子邮箱地址”等可选字段不适合作为主键。 其次,主键最好不要变动以维护表的稳定,因而“用户名”等可修改的字段也不合适。总的来说,主键最好与业务无关以摆脱易变的业务的影响,所以一个很好的选择就是用机器自动生成的 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
服务器(或集群)单独来做。
具体来说,就是利用 Redis
的 incr
原子性操作自增。
这样便轻松解决了 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