1. 分布式id生成需满足的条件

  1. 全局唯一:分布式ID必须全局唯一,确保数据可以被唯一确定。
  2. 高性能:高并发场景下分布式ID必须快速响应生成。
  3. 高并发:分库分表数据存储大概率会出现高并发量的请求,所以这套方案必须在高并发场景下快速生成id。
  4. 高可用::需要无限接近百分百的可用,避免因为因为分布式ID生成影响其他业务运行。
  5. 易用:方案必须易于使用,不大量侵入业务代码。
  6. 递增:尽可能保证递增,确保ID有序插入以保证插入性能和索引维护开销。

2. 常见id生成方法

2.1 UUID

UUID有着全球唯一的特性,符合全局唯一、高性能、高并发、高可用、易用的特性。但是它却有以下缺点:

  1. 字符串类型:因为是字符类型,占用大量存储空间。
  2. 无序:因为生成的无规律,因为大量的随机添加势必导致MySQL底层大量的B+ tree的节点分裂,耗费大量计算资源,严重影响数据库性能,进而导致查询耗时增加。

所以不推荐

2.2 数据库自增id

单表生成的id不是全局唯一,不考虑。

2.3 号段式id分配策略

我们可以使用号段的方式来获取自增ID,号段可以理解成批量获取,比如DistributIdService从数据库获取ID时,如果能批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。

比如DistributIdService每次从数据库获取ID时,就获取一个号段,比如(1,1000],这个范围表示了1000个ID,业务应用在请求DistributIdService提供ID时,DistributIdService只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段。

1
2
3
4
5
6
7
8
CREATE TABLE id_allocation (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '当前已用最大id',
  step int(20) NOT NULL COMMENT '号段分配步长',
  biz_type    int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
)

这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值),因为自增逻辑被移到DistributIdService中去了,所以数据库不需要这部分逻辑了。

这种方案不再强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启,会丢失一段ID,导致ID空洞。

为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7…. mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10…

2.4 基于redis原子自增指令INCR

由于Redis基于内存操作并且是单线程执行命令,所以性能高并且也不会有重复id。

当然这种方案也有着一定的缺点:

  1. 若我们使用rdb持久化机制,一旦redis宕机等原因导致缓存丢失,再次从redis中获取的id很可能出现冲突。
  2. 若使用aof会导致每一条命令都会进行持久化,但也会导致重启数据恢复时间过长。

2.5 雪花算法

优点:

  1. 高性能高可用:生成时不依赖于数据库,完全在内存中生成。
  2. 高吞吐:每秒钟能生成数百万的自增 ID。
  3. ID 自增:存入数据库中,索引效率高。

2.5.1 雪花算法组成

image-20250330132437219

包含四个组成部分:

  • 不使用:1bit,最高位是符号位,0 表示正,1 表示负,固定为 0
  • 时间戳:41bit,毫秒级的时间戳(41 位的长度可以使用 69 年
  • 标识位:5bit 数据中心 ID,5bit 工作机器 ID,两个标识位组合起来最多可以支持部署 1024 个节点
  • 序列号:12bit 递增序列号,表示节点毫秒内生成重复,通过序列号表示唯一,12bit 每毫秒可产生 4096 个ID。通过序列号 1 毫秒可以产生 4096 个不重复 ID,则 1 秒可以生成 4096 * 1000 = 409w ID。

默认的雪花算法是 64 bit,具体的长度可以自行配置。如果希望运行更久,增加时间戳的位数;如果需要支持更多节点部署,增加标识位长度;如果并发很高,增加序列号位数。

标识位中的数据中心ID可以通过mac地址来生成,工作机器ID可以通过PID生成。

但是这样仍有小概率重复,对于MybatisPlus的雪花算法来说:

  • MAC 地址由 IEEE 分配的前 24 bit + 厂商自己负责唯一性的 24 比特组成,数据中心 ID 只使用了 MAC 地址的后 2 字节,而不同厂商生产的后 24 bit 是有可能相同的
  • k8s 扩容过程中短时间内大量启动的进程可能会导致 PID 的重复使用。

可以通过Redis来动态分配标识位,Redis 存储一个 Hash 结构 Key,包含两个键值对:dataCenterIdworkerId。在应用启动时,通过 Lua 脚本去 Redis 获取标识位。dataCenterIdworkerId 的获取与自增在 Lua 脚本中完成,调用返回后就是可用的标示位。

具体 Lua 脚本逻辑如下:

  1. 第一个服务节点在获取时,Redis 可能是没有 snowflake_work_id_key 这个 Hash 的,应该先判断 Hash 是否存在,不存在初始化 Hash,dataCenterId、workerId 初始化为 0。
  2. 如果 Hash 已存在,判断 dataCenterId、workerId 是否等于最大值 31,满足条件初始化 dataCenterId、workerId 设置为 0 返回。
  3. dataCenterId 和 workerId 的排列组合一共是 1024,在进行分配时,先分配 workerId。
  4. 判断 workerId 是否 != 31,条件成立对 workerId 自增,并返回;如果 workerId = 31,自增 dataCenterId 并将 workerId 设置为 0。

dataCenterId、workerId 是一直向下推进的,总体形成一个环状。通过 Lua 脚本的原子性,保证 1024 节点下的雪花算法生成不重复。如果标识位等于 1024,则从头开始继续循环推进。

这是在标示位为10bit的情况,支持1024个节点,超出范围就需要采取其他措施:扩展 10 bit 标识位,或者选择开源分布式 ID 框架。

在使用雪花算法的时候应注意时钟回拨问题

我们都知道雪花id是通过时间戳结合自增序列等方式保证分布式id的唯一性,但是这种方案其实也存在一定的缺陷。

我们假设这样一个场景,我们现在的雪花id工具类在服务器1上,所以在这台服务器上的id对应的机器id服务id都是相等的。唯一性都是通过时间戳+自增序列来保证,试想一下如果我们在雪花id工具工作期间,将操作系统时间回拨,这就可能出现之前用过时间戳+自增序列重复,进而导致数据入库失败。

我们可以在每次生成雪花id时,维护一下生成的时间戳,每次生成时通过比对本次时间戳和上次时间戳的差值判断是否出现时钟回拨。

3. 实际使用

在12306项目中,将用户的后六位id拼在了分布式id的后面,这样用户就可以通过订单号或者自己的id定位到订单所在的数据库,避免读扩撒。