Skip to content

唯一ID

一、前言

在业务开发,会存在大量的场景使用唯一ID来进行标识,对于一个唯一ID,特性如下:

  • 全局唯一:必须保证生成的ID是全局唯一的
  • 有序性:生成的ID需要按照某种规则是有序的,防止插入数据库时出现页分裂的问题
  • 安全性:不暴露系统和业务的信息

二、UUID

生成唯一ID,其中最为有效的方式就是通过 UUID 来获取,并且这种方式重复的概率非常低,在这种情况之下每个 Web 系统都含有一个ID生成器负责独立的生成ID。

但是 ID 并不能随时间增长,ID 可能也并不是纯数字的。如果采用这种方式作为数据库的主键就会造成页分裂的问题,影响插入的效率。

三、自增ID

这个方法的核心就是利用数据库的自增ID来实现唯一ID,强依赖于数据库,当 DB 不可用时,整个系统就会处于不可用的状态,并且 ID 的生成效率依赖于数据库的读写性能。除了采用数据库之外,还可以采用 Redis 的 incr 命令来实现。

这两种方式虽然能够生成唯一ID,但是对于数据库和Redis的压力比较大。

针对于这种方案,可以进行如下的优化:

  • 批量生成:服务启动时,一次性生成一批次的 ID,放在内存之中,直接从内存中返回即可
  • 水平拆分:多个分片,不同的初始值,相同的步长,即第一个分片 1,3,5,7,9..... , 第二个分片 2,4,6,8,10...

四、数据库的号段模式

号段模式,可以理解为批量从数据库获取一部分ID,然后存在内存之中,获取 ID 的时候直接从内存之中返回即可。比如有两台服务器,第一台服务器申请到的号段是:[1,1000],第二个服务器申请到的号段是:[1001,2000],第一台服务器获取ID的时候,就从1开始递增,直到申请到的号段使用完成。通过这种方式,可以大大减少数据库的使用压力。

接下来,我们就来看一下美团 Leaf-segment 方案。对于美团的方案,其实就是基于数据库批量生成,其对应的流程如下:在数据库之中建立一张表,用来表示不同的业务当前使用到的最大ID,以及每次申请的步长,当我们申请 ID 的时候,只需要执行 update XX set max_id = max_id + step ,当 update 成功的时候,就说明申请到了 [max_id, max_id + step] 范围的号段

使用这种方式,有如下的缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • 当号段使用完之后,去再次去申请号段,会有阻塞,并且会耗时在数据库的 IO
  • DB宕机会造成整个系统不可用。

为了解决问题二,采用双buffer的方式,服务内部有两个号段缓存区 segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

但是,我看了一下 GitHub 的代码,不知道是否维护?

五、雪花算法

对于雪花算法,由 64 位组成,构成图如下:

image-20250223231538742

其中,这几部分的含义如下:

  • 符号位:1 位,始终作为数字 0
  • 时间戳:41 位
  • 数据中心:5 位,最多可以有 32 个数据中心
  • 机器ID:5位,每个数据中心最多可以有 32 个机器
  • 序列号:12 位,对于某个机器或者进程,每生成一个 ID,序列号就加一,这个数字每毫秒开始都被重置为 0

对于 数据中心ID 和 机器ID 通常在起始阶段就选好了,一旦系统运行起来,这两个部分都是固定的,对于时间戳和序列号在运行后生效。

这种方案的优势在于:本次生成,没有网络开销,性能较高,整体呈增长趋势。劣势在于时钟回拨问题。

为了解决时钟回拨问题,可以采用如下方案:

  • 将 ID 生成交给少量服务器,关闭这些服务器的时钟回拨能力,这样做对于ID的生成就不是本地了,而是需要调用对应的接口来获取唯一ID