【分布式】一文详解五大分布式ID实现方案

分布式ID

分布式 ID 是在分布式环境中生成全局唯一标识符的技术,确保在不同节点生成的 ID 不会冲突。常用于数据库主键订单号等场景。

分布式 ID 是分布式系统中不可或缺的一环,其设计直接影响系统的性能和稳定性。在实际应用中,可以根据业务需求选择合适的生成方案,如高并发场景优先选择雪花算法,有序性需求可选 Redis 或号段模式。

在这里插入图片描述

分布式 ID 需要满足下面几个特性:

  • 全局唯一:每个ID在整个分布式系统中必须是全局唯一的,不能出现重复。

  • 高性能&高可用:在高并发的情况下,分布式ID生成系统需要具备高可用性和低延迟,能够快速生成ID。

  • 有序性:ID 的有序性可以提升数据库的写入效率,以便排序或提高数据库性能。

  • 安全性:需要保证 ID 无法被轻易猜测,避免数据库信息被泄露。

数据库自增主键

数据库的自增主键AUTO_INCREMENT)是数据库层面提供的一种功能,能够自动为新插入的记录生成一个递增的整数主键。利用这种特性,应用程序可以通过插入一条记录获取唯一的 ID。

实现方法

1. 创建数据库表

CREATE TABLE `sequence_id` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `stub` char(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `stub` (`stub`)
);
  • id 字段:作为主键,保证 id 字段全局唯一,并通过 AUTO_INCREMENT,自增生成唯一的分布式 ID
  • stub 字段:设为唯一索引(UNIQUE KEY),无实际业务意义,仅用于支持 REPLACE INTO 操作。强制插入数据时不能有重复值。

2. 执行事务生成并获取唯一ID

BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;
  • 开启事务:利用事务的隔离性,防止其他线程获取到当前的分布式ID,破坏分布式ID的唯一性。
  • REPLACE INTO 语义:如果 stub 值已经存在,则删除该行并插入新行;如果 stub 值不存在,则直接插入新行。通过 REPLACE INTO 插入元素,生成自增主键ID。
  • SELECT LAST_INSERT_ID() 作用:返回当前会话最后一次生成的自增主键值。

通过执行事务,可以获取到数据库生成的唯一ID:

此时 sequence_id 表的数据如下:

优缺点

使用数据库自增主键生成分布式ID的方案实现简单,适合中小型系统,以下是优缺点:

优点

  • 唯一性:在同一个数据库实例内是唯一的,天然保证了 ID 不会重复;在数据库集群模式下,需要额外设计步长+偏移量的方案,保证唯一性。
  • 有序性:在数据库存储时,自增 ID 的有序性可以减少 B+ 树索引的分裂,提高写入性能;但在数据库集群模式下,不能保证分布式ID严格有序。

缺点

  • 性能问题:频繁的 INSERT 操作会引发锁的竞争,降低数据库性能;每次获取分布式 ID 都要访问数据库,查询受到磁盘I/O的限制。
  • 可用性问题:存在单点故障问题,如果负责生成 ID 的数据库实例宕机,整个系统将无法生成新 ID。
  • 数据膨胀问题:频繁的写操作会导致数据库的二进制日志快速膨胀,影响复制和备份效率。
  • 信息泄露问题:自增 ID 直接暴露数据插入的顺序,可能导致业务敏感信息(如用户量、交易量)被推测。

数据库号段模式

号段模式是一种高效生成分布式唯一ID的方案,适用于分布式系统中需要高性能、全局唯一ID的场景。号段模式通常依赖于数据库,通过将分配到的号段缓存到内存中,直接从号段中取值生成ID,避免频繁访问数据库。

号段:将一批连续的数字ID作为一个号段,例如 [1001, 2000]。客户端从号段中依次分配ID,直到用完,再申请新的号段。

实现方法

1. 创建数据库表

CREATE TABLE sequence_id_generator (
    biz_type VARCHAR(50) NOT NULL,      -- 业务类型,如订单ID、用户ID等
    max_id BIGINT NOT NULL,            -- 当前号段的最大值
    step INT NOT NULL,                 -- 步长(每次分配的号段大小)
    version INT NOT NULL,              -- 乐观锁版本号,防止并发问题
    PRIMARY KEY (biz_type)
);

表中各个字段的含义如下:

字段含义
biz_type业务类型,用于区分不同业务的ID生成规则
max_id当前号段的最大值,表示分配的ID范围上限
step每次申请号段时的步长,为实际可用的ID数量
version版本号,申请新的号段时根据版本号进行并发控制

2. 初始化数据

在使用号段模式分配ID之前,需要先在数据库中初始化数据,方便后续客户端的取用号段。

这里将初始的号段最大值 max_id 设置为 0,步长 step 设置为 100,版本号 version 设置为 0。

INSERT INTO `sequence_id_generator`(`biz_type`, `max_id`, `step`, `version`) 
VALUES (101, 0, 100, 0);

3. 获取分布式ID

SELECT `max_id`, `step`,`version` 
FROM `sequence_id_generator` 
WHERE `biz_type` = 101;

返回结果如下:

分配的号段 [max_id + 1, max_id + step] 为分布式ID的可用范围。申请到号段的线程会将该号段缓存起来,直接在内存中取用号段中的数据作为分布式ID,从而避免访问数据库。(如果使用的是数据库自增主键的方案,就需要查询 step 次数据库,影响性能)

4. 生成分布式ID

如果某一业务类型的号段用尽时,需要从数据库中申请新的号段。在应用层执行 UPDATE 语句时需要使用乐观锁,防止并发情况下多个线程申请到相同的号段。

UPDATE sequence_id_generator 
SET max_id = max_id + step, version = version + 1 
WHERE biz_type = 101 AND version = #{version};

数据库更新结果:

优缺点

优点:

  • 唯一性:依靠 MySQL 数据库的行级锁和应用层的乐观锁,保证相同业务类型 biz_type下号段分配的唯一性。
  • 有序性:每个号段分配的 ID 都是单调递增的,这对于某些需要有序 ID 的场景(如订单号、时间戳排序等)非常适用。
  • 高性能:相较于数据库自增主键的方案,ID 分配主要在内存中完成,只有在号段用尽时才访问数据库申请新的号段,大大提高了生成效率。

缺点

  • 乐观锁重试问题:在高并发情况下,客户端申请号段时,乐观锁可能频繁失败,需要多次重试才能成功获取号段,增加了分配延迟。
  • 可用性问题:存在单点故障问题,如果负责生成号段的数据库实例宕机,整个系统将无法生成新 ID。
  • 信息泄露问题:自增 ID 直接暴露数据插入的顺序,可能导致业务敏感信息(如用户量、交易量)被推测。

Redis的incr命令

RedisINCR 命令是一种简单高效的分布式 ID 生成方案。通过 Redis单线程模型来保证分布式 ID 的唯一性,而 INCR 命令则保证了分布式 ID 的有序性。

单线程模型Redis 的单线程模型保证多个客户端执行 INCR 命令时,同一时刻只会有一个线程执行 INCR 命令,因此避免了并发安全问题。

实现方法

1. 初始化数据

首先设置分布式 ID 的初始结构,后续直接取用 seq_id 对应的 value值作为分布式 ID。

SET seq_id 1

在这里插入图片描述

2. 生成分布式ID

对于单机模式下的 Redis,由于只存在一个 Redis 节点,直接使用 INCR 命令即可保证有序性。

对于集群模式下的 Redis,由于存在多个 Redis 节点,因此需要给每个节点设置不同的初始偏移量,并使用 INCRBY 命令指定集群中所有节点数量的步长来保证全局唯一性。(也可以使用哈希插槽将生成的分布式 ID 的键固定到同一个节点,将问题转化单机模式,Redis 集群模式讲解详见此处

比如集群中共存在3个节点A、B、C,可以指定A的初始值为0,B的初始值为1,C的初始值为2,步长设置为3。这样每个节点生成的分布式ID为 初始偏移量 + 访问次数 * 步长,防止不同节点生成重复的分布式ID,保证 ID 的全局唯一性。

# 自增命令
INCR seq_id
# 指定步长自增
INCRBY seq_id 3

3. 获取分布式ID

GET seq_id

优缺点

优点:

  • 唯一性:单机模式下依赖于 Redis 的单线程模型,可以保证 ID的唯一性;在集群模式,可以通过哈希插槽固定键到某个节点,或者通过步长+偏移量实现分布式 ID 的生成。
  • 有序性:在 Redis 单机模式或者在集群模式使用哈希插槽固定键到某个节点时,可以保证 ID 是严格有序的。
  • 高性能Redis 是内存数据库,INCR 操作非常快,延迟一般在微秒级,单节点 QPS 可达十万级别。
  • 高可用:通过部署 Redis 分片集群,可以解决单点故障问题,保证某些节点故障后分布式 ID 生成服务依旧可用。

缺点

  • 非严格有序:在分布式场景下,为了保证ID 的唯一性,通常通过步长+偏移量生成 ID,但这导致生成的 ID 不是严格有序的。
  • 信息泄露问题:自增 ID 直接暴露数据插入的顺序,可能导致业务敏感信息(如用户量、交易量)被推测。

UUID

UUID (Universally Unique Identifier) 是一种通用唯一标识符,其目的是为分布式系统中的所有元素生成唯一的标识符,在大多数情况下具有全球唯一性。UUID 的长度为 128 位(16 字节 ),通常用 32 个十六进制数字表示。

按照十六进制的数字个数划分,UUID的长度构成为 8-4-4-4-12,一共 32 个数字。以下为 UUID 的示例:

550e8400-e29b-41d4-a716-446655440000

UUID 的内部结构分为 5 个主要部分,每部分基于不同的位数:

  1. 时间低位(前 8 个字符):共 32 位,表示时间戳的地位部分。
  2. 时间中位(占 4 个字符):共 16 位,表示时间戳的中间部分。
  3. 时间高位及版本号(占 4 个字符):共 16 位,由前 4 位版本号和后 12 位的时间戳高位部分组成。
  4. 变体及序列号(占 4 个字符):共 16 位,前 3 位表示变体,后 13 位表示序列号。
  5. 节点(占 12 个字符):共 48 位,表示节点信息,通常是设备的 MAC 地址或随机数。

UUID 的生成方式由其 版本号(Version)变体(Variant) 决定,本文主要关注不同版本下 UUID。

UUID版本

UUID 的五个版本各有用途,但最常用的是版本一版本四版本三版本五更适合生成基于命名空间的确定性标识符,而版本二因适用范围窄,在现代系统中已很少使用。

  • 版本一:基于时间戳和节点地址的 UUID

    通过计算当前时间戳、时钟序列号和机器的 MAC 地址生成。

    • 由于使用了 MAC 地址,可以保证在全球范围内唯一性。
    • 包含生成时间信息,时间戳可以用作生成时间的标识。
    • 依赖硬件 MAC 地址,可能会暴露设备信息,存在隐私风险。
  • 版本二:DCE 安全的 UUID

    类似于版本一,但是依赖于早期的 DCE (分布式计算环境)环境,增加了一些安全特性,在现代系统中很少使用。

  • 版本三:基于命名空间的UUID(MD5)

    通过对名字(如字符串)和命名空间(如 URL)的组合计算 MD5 哈希值,并将 MD5 哈希值的部分位替换为版本号和变体标志生成。

    • 相同命名空间中相同名字生成的 UUID 保持一致。
    • 哈希算法确定性强,适用于需要固定标识符的场景。
    • 使用 MD5 算法,安全性较弱。
  • 版本四:基于随机数的UUID

    UUID 的 128 位数据大部分由随机数生成。

    • 无需依赖硬件或时间信息,生成速度快。
    • 碰撞概率极低,即使生成 10¹² 个 UUID,也几乎不会发生冲突。
    • 不包含设备或时间信息,隐私性较好。
  • 版本五:基于命名空间的UUID(SHA1)

    与版本三类似,但使用更安全的 SHA-1 哈希算法代替 MD5。

    • 同样的命名空间和名字生成的 UUID 一致。

    • SHA-1 比 MD5 更安全,但仍然存在一定的碰撞风险(对于 UUID 应用影响有限)。

UUID版本重复条件重复概率
版本1如果两个设备具有相同的 MAC 地址、相同的时钟序列,并在同一时刻生成,则可能重复。几乎不可能
版本2如果两个设备使用相同的本地标识符(UID/GID)和相同时间戳,可能重复(需要运行在 DCE 环境)。几乎不可能
版本3相同命名空间相同名字才会重复;不同命名空间或名字不会重复。确定性重复
版本5相同命名空间相同名字才会重复;不同命名空间或名字不会重复。确定性重复
版本4由于随机数生成碰撞(在生成量极大时,如接近 2 64 2^{64} 264 时可能发生)。极低概率

优缺点

优点

  • 唯一性:使用不同的算法生成,几乎可以保证在全球范围内唯一,避免了多台机器之间主键冲突的问题。
  • 无需网络请求:UUID 生成过程完全依赖本地算法,无需访问数据库或其他第三方服务,不依赖于网络请求,降低了延迟。
  • 高可用:UID 生成过程完全依赖本地算法,无需依赖于第三方服务。
  • 不可预测:UUID 没有明确的序列信息,能够在一定程度上保护数据隐私,不易被外界推测业务数据规模或增长趋势。

缺点

  • 存储开销大:UUID的长度为128位(16字节),存储时会占用较多空间,尤其是在数据库中作为主键时,会增加索引的存储成本。
  • 不具体有序性:UUID生成通常是随机的(如版本四),插入数据库时会导致索引频繁分裂或重排,降低写入性能。

雪花算法

雪花算法(Snowflake 算法) 是 Twitter 开源的分布式唯一 ID 生成算法,主要用于分布式系统中生成全球唯一的、有序的 64 位长整型 ID。它能够在高并发环境下快速生成高效、唯一的 ID,并确保生成的 ID 大致有序。

  • 符号位(1bit):预留的符号位,始终为0,占用1位。
  • 时间戳(41bit):记录生成 ID 时的时间戳,精确到毫秒级别,可以容纳的毫秒数是2的41次幂,可以支持大约 69 年的时间跨度。
  • 数据中心标识(5bit):可以用来区分不同的数据中心。
  • 机器标识(5bit):可以用来区分不同的机器。
  • 序列号(12bit):表示同一毫秒内生成的 ID 的序号,单台机器每毫秒可以生成 4096 个唯一ID。

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

防止时钟回拨

雪花算法生成的唯一ID依赖于当前系统时间戳。如果发生时钟回拨(系统时间向后调整),导致新的时间戳与之前生成 ID 时的时间戳重复,可能会造成分布式 ID 不唯一的问题。

  1. 等待策略:使用一个变量 lastTimestamp 记录上次生成 ID 的时间戳。当检测到时钟回拨时,服务可以暂时拒绝生成新的ID,等待系统时钟恢复到正常状态(至少大于或等于上次生成ID时的时间戳)。这种策略可以确保ID的严格递增性,但可能会在时钟调整期间暂停服务,对系统性能造成一定的影响。
  2. 序列号持久化:雪花算法原本设计中序列号是在内存中自增的,为了避免时钟回拨导致的序列号重置问题,可以将序列号持久化存储到磁盘上。这样即使服务重启或时钟回拨,服务也能继续从上次持久化的序列号开始自增,从而避免ID重复。这种策略可以在一定程度上容忍时钟回拨,但可能会影响ID的绝对时间戳准确性。

保证标识位唯一

雪花算法生成的唯一ID依赖于标识位。在集群模式下产生重复 ID,需要以下两个前提条件:

  1. 服务通过集群的方式部署,其中部分机器标识位一致。(比如在 K8s 中,多个 Pod 使用相同的配置镜像启动,而镜像中未动态生成唯一标识)
  2. 在高并发场景下,不同实例或业务在同一毫秒内生成了相同的序列号,由于机器标识位一致,导致生成的 ID 出现冲突。

由此可知,只要能够保证标识位不重复,那么雪花算法生成的分布式ID也不会重复。

预分配

在应用启动之前,统计当前服务的节点数量,人工去申请标识位。

这种方案适合服务节点数量基本不变或者服务节点数量少的情况,但是如果服务节点较多,或者经常需要扩缩容时,预分配是无法满足ID不重复的要求的。

动态分配

将标识位存放在 Redis、Zookeeper、MySQL等中间件中,在服务启动时动态的请求标识位。

如果要保证分布式ID在服务内唯一,将标识位直接存放在服务所使用的 Redis 节点即可;如果要保证分布式ID的全局唯一,则多个服务需要使用同一个 Redis 节点保证唯一性。

数据中心标识和机器标识的组合为 32 * 32,即可以存储 1024 个服务节点,如果节点数超过了这个数量,则可以扩展标识位,或者选用其他开源的分布式ID框架。

具体方案

下面将用 Redis 展示服务内唯一的标识号分配实现方案

1. 设计键值结构

使用 Redis 中的 Hash 结构,KEY 为,键值对为 datacenterIdworkerId 及其对应生成的标识号。

在这里插入图片描述

2.标识号自增逻辑

通过 Lua 脚本从 Redis 中获取标识位,具体的执行逻辑为:

  1. 检查 Hash 是否存在:如果 Redis 中不存在 id_key 这个 Hash,则初始化 id_key,并将 dataCenterIdworkerId 设置为 0。
  2. 判断 Hash 是否已存在
    • 如果 id_key 已存在,检查 dataCenterIdworkerId 是否都达到最大值(31)。
    • 若条件满足,则将 dataCenterIdworkerId 都重置为 0,并返回。
  3. 分配策略dataCenterIdworkerId 的排列组合一共有 1024 种可能性,在分配时优先分配 workerId
  4. 分配逻辑
    • 检查 workerId 是否小于 31。若满足条件,则对 workerId 自增并返回。
    • 如果 workerId 等于 31,则将 dataCenterId 自增,同时将 workerId 重置为 0,并返回。

3.标识号循环递增

在当前自增逻辑中,采用了 dataCenterIdworkerId 循环递增的方案。将 dataCenterId 视为高位,workerId 等于最大值 31 时向 dataCenterId 进一位;dataCenterIdworkerId 都达到最大值 31 时数据重置为 0-0。数据展示如下图:

因此该方案最多只能保证 1024 个服务节点分配的标识位不重复。如果当前系统的服务节点超过了 1024 个,会出现标识位分配重复的情况,进而增加分布式ID重复的风险。

优缺点

优点

  • 高性能高可用:生成时不依赖于数据库,完全在内存中生成,每秒钟能生成数百万的自增ID。
  • ID自增:在单个进程中,生成的ID是自增的,可以用作数据库主键做范围查询。但是需要注意的是,在集群中是没办法保证顺序一定是递增的。

缺点

  • 重复ID问题:雪花算法依赖于时间戳,在获取时间的时候,如果出现时间回拨问题,服务器上的时间突然倒退到之前的时间,会造成 ID 重复的问题。
  • 依赖机器ID不灵活:每个节点的机器ID和数据中心ID都是硬编码在代码中的,在分布式环境如果节点出现故障或者需要扩容,更改机器ID和数据中心ID需要重新编译代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值