Skip to content

分布式 ID 实现

分布式 ID

分布式 ID 是分布式系统下的 ID。一个最基本的分布式 ID 需要满足下面这些要求:

  • 全局唯一:ID 的全局唯一性肯定是首先要满足的。
  • 高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。
  • 高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。
  • 方便易用:拿来即用,使用方便,快速接入。

除了这些之外,一个比较好的分布式 ID 还应保证:

  • 安全:ID 中不包含敏感信息。
  • 有序递增:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候,我们还很有可能会直接通过 ID 来进行排序。
  • 有具体的业务含义:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
  • 独立部署:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。

分布式 ID 常见解决方案

关系型数据库

数据库主键自增

通过关系型数据库的自增主键产生来唯一的 ID。

数据库主键自增的优缺点:

  • 优点:实现起来比较简单、ID 有序递增、存储消耗空间小。
  • 缺点:支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量)、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)

数据库号段模式

数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面取就可以了。这也就是 基于数据库的号段模式来生成分布式 ID。数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的 Tinyid 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 DB 支持等方式来进一步优化。

以 MySQL 为例,通过下面的方式即可:

  1. 创建一个数据库表
ts
CREATE TABLE `sequence_id_generator` (
    `id`             int(10)    NOT NULL,
    `current_max_id` bigint(20) NOT NULL COMMENT '当前最大 ID',
    `step`           int(10)    NOT NULL COMMENT '号段的长度',
    `version`        int(20)    NOT NULL COMMENT '版本号',
    `biz_type`       int(20)    NOT NULL COMMENT '业务类型',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

current_max_id 字段和 step 字段主要用于获取批量 ID,获取的批量 ID 为:current_max_id ~ current_max_id + stepversion 字段主要用于解决并发问题(乐观锁),biz_type 主要用于表示业务类型。

  1. 先插入一行数据
ts
INSERT INTO `sequence_id_generator` (
    `id`,
    `current_max_id`,
    `step`,
    `version`,
    `biz_type`
) VALUES (
    1,
    0,
    100,
    0,
    101
);
  1. 通过 SELECT 获取指定业务下的批量唯一 ID
ts
SELECT `current_max_id`, `step`, `version`
FROM   `sequence_id_generator`
WHERE  `biz_type` = 101;
  1. 不够用的话,更新之后重新 SELECT 即可
ts
UPDATE sequence_id_generator
SET    current_max_id = 0 + 100, version = version + 1
WHERE  version = 0 AND `biz_type` = 101;
ts
SELECT `current_max_id`, `step`, `version`
FROM   `sequence_id_generator`
WHERE  `biz_type` = 101;

相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。另外,为了避免单点问题,可以使用主从模式来提高可用性。

数据库号段模式的优缺点:

  • 优点:ID 有序递增、存储消耗空间小。
  • 缺点:存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量)。

非关系型数据库

Redis

一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 ID 原子顺序递增。

bash
127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
"2"

为了提高可用性和并发,可以使用 Redis Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案。除了 Redis Cluster 之外,也可以使用开源的 Redis 集群方案 Codis(大规模集群比如上百个节点的时候比较推荐)。

除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:快照(Snapshotting,RDB)只追加文件(Append-only file, AOF)。并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

Redis 方案的优缺点:

  • 优点:性能不错并且生成的 ID 是有序递增的。
  • 缺点:和数据库主键自增方案的缺点类似。

MongoDB

除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。

MongoDB ObjectId 一共需要 12 个字节存储:

  • 0~3:时间戳
  • 3~6:代表机器 ID
  • 7~8:机器进程 ID
  • 9~11:自增值

MongoDB 方案的优缺点:

  • 优点:性能不错并且生成的 ID 是有序递增的
  • 缺点:需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)、有安全性问题(ID 生成有规律性)

算法

UUID

UUID 是 Universally Unique Identifier(通用唯一标识符)的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。

不同的版本对应的 UUID 的生成规则是不同的。8 种不同的 Version(版本)值分别对应的含义:

  • 版本 1(基于时间和节点 ID): 基于时间戳(通常是当前时间)和节点 ID(通常为设备的 MAC 地址)生成。当包含 MAC 地址时,可以保证全球唯一性,但也因此存在隐私泄露的风险。
  • 版本 2(基于标识符、时间和节点 ID): 与版本 1 类似,也基于时间和节点 ID,但额外包含了本地标识符(例如用户 ID 或组 ID)。
  • 版本 3(基于命名空间和名称的 MD5 哈希):使用 MD5 哈希算法,将命名空间标识符(一个 UUID)和名称字符串组合计算得到。相同的命名空间和名称总是生成相同的 UUID(确定性生成)。
  • 版本 4(基于随机数):几乎完全基于随机数生成,通常使用伪随机数生成器(PRNG)或加密安全随机数生成器(CSPRNG)来生成。 虽然理论上存在碰撞的可能性,但理论上碰撞概率极低(2^122 的可能性),可以认为在实际应用中是唯一的。
  • 版本 5(基于命名空间和名称的 SHA-1 哈希):类似于版本 3,但使用 SHA-1 哈希算法。
  • 版本 6(基于时间戳、计数器和节点 ID):改进了版本 1,将时间戳放在最高有效位(Most Significant Bit,MSB),使得 UUID 可以直接按时间排序。
  • 版本 7(基于时间戳和随机数据):基于 Unix 时间戳和随机数据生成。 由于时间戳位于最高有效位,因此支持按时间排序。并且,不依赖 MAC 地址或节点 ID,避免了隐私问题。
  • 版本 8(自定义):允许用户根据自己的需求定义 UUID 的生成方式。其结构和内容由用户决定,提供更大的灵活性。

UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。

UUID 的优缺点:

优点:生成速度通常比较快、简单易用。 缺点:存储消耗空间大(32 个字符,128 位)、不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)。

Snowflake(雪花算法)

Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:

  • sign (1 bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
  • timestamp (41 bits):一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 214 毫秒(约 69 年)。
  • datacenter id + worker id (10 bits):一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群 / 机房的节点。
  • sequence (12 bits):一共 12 位,用来表示序列号。序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(212=4096),也就是说单台机器每毫秒最多可以生成 4096 个唯一 ID。

在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。

Snowflake 算法的优缺点:

  • 优点:生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)。
  • 缺点:需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能会出现时间回拨的问题,也就是服务器上的时间突然倒退到之前的时间,进而导致会产生重复 ID)、依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。

有很多基于 Snowflake 算法的开源实现比如美团的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化,性能更优秀,还解决了 Snowflake 算法的时间回拨问题和依赖机器 ID 的问题。并且,Seata 还提出了“改良版雪花算法”,针对原版雪花算法进行了一定的优化改良,解决了时间回拨问题,大幅提高的 QPS。

开源框架

UidGenerator(百度)

UidGenerator 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下:

  • sign (1 bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
  • delta seconds (28 bits):当前时间,相对于时间基点“2016-05-20”的增量值,以秒为单位,最多可支持约 8.7 年。
  • worker id (22 bits):机器 ID,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
  • sequence (13 bits):每秒下的并发序列,13 bits 可支持每秒 8192 个并发。

可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。

Leaf(美团)

Leaf 是美团开源的一个分布式 ID 解决方案 。

Leaf 提供了 号段模式雪花算法 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper(使用 Zookeeper 作为注册中心,通过在特定路径下读取和创建子节点来管理 workId)。

Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。

Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。

Tinyid(滴滴)

Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。

基于数据库号段模式的简单架构方案

在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。

这种方案有什么问题呢?

  • 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。
  • 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。

除此之外,HTTP 调用也存在网络开销。

Tinyid 的原理比较简单,其架构如下图所示:

相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:

  • 双号段缓存:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
  • 增加多 DB 支持:支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
  • 增加 tinyid-client:纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。

IdGenerator

和 UidGenerator、Leaf 一样,IdGenerator 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器。

IdGenerator 有如下特点:

  • 生成的唯一 ID 更短。
  • 兼容所有雪花算法(号段模式或经典模式,大厂或小厂)。
  • 原生支持 C# / Java / Go / C / Rust / Python / Node.js / PHP (C 扩展) / SQL 等语言,并提供多线程安全调用动态库(FFI)。
  • 解决了时间回拨问题,支持手工插入新 ID(当业务需要在历史时间生成新 ID 时,用本算法的预留位能生成 5000 个每秒)。
  • 不依赖外部存储系统。
  • 默认配置下,ID 可用 71000 年不重复。

IdGenerator 生成的唯一 ID 组成如下:

  • timestamp(位数不固定):时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年)的总时间差(毫秒单位)。初始为 5 bits,随着运行时间而增加。如果觉得默认值太老可以重新设置,不过要注意,这个值以后最好不变。
  • worker id(默认 6 bits):机器 ID,机器码,是最重要的参数,是区分不同机器或不同应用的唯一 ID,最大值由 WorkerIdBitLength(默认 6)限定。如果一台服务器部署多个独立服务,需要为每个服务指定不同的 WorkerId。
  • sequence(默认 6 bits):序列数,是每毫秒下的序列数,由参数中的 SeqBitLength(默认 6)限定。增加 SeqBitLength 会让性能更高,但生成的 ID 也会更长。