分布式ID生成

什么是分布式ID?

分布式 ID 是指在一个分布式系统中,为每一个数据项或事件生成一个全局唯一标识符的过程。 这个标识符通常是一个长整型数字或字符串,能够跨多个服务实例和数据库集群唯一识别每一个实体,是实现数据关联和跟踪的基础。

在传统的单体应用中,ID 生成相对简单,可以通过数据库的自增字段来实现。但在微服务架构下,每个服务可能运行在不同的服务器上,甚至可能有多个实例,这就意味着每个服务都需要独立生成 ID,并且保证全局唯一性。此外,分布式 ID 还需要解决以下几个关键问题:

  • 一致性:所有生成的 ID 必须在分布式环境中保持一致,避免重复和冲突。

  • 高性能:在高并发场景下,ID 生成机制不能成为系统的瓶颈。

  • 可扩展性:随着业务的增长,ID 生成策略应该易于扩展,适应更大的负载。

  • 容错性:即使部分服务出现故障,ID 生成也不能中断。

分布式 ID 生成方案

目前,业界已经发展出了多种分布式 ID 生成算法和技术,以下是常见的几种方案:

  1. UUID : UUID (Universally Unique Identifier) 是一种常用的分布式 ID 生成方式, 它的标准型式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符,示例:550e8400-e29b-41d4-a716-446655440000。

    优点:

    • 生成性能非常高:直接本地生成,不依赖其他中间件,无网络 / 磁盘 IO 消耗;

    缺点:

    • 不易于存储:UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示。在海量数据场景下,会消耗较大的存储空间。

    • 信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

    • 充当主键时,在特定场景下,会存在问题。如作为 MySQL 数据库的主键时,UUID 就非常不合适。

    • 基于数据库(DB)的自增 ID :可以单独创建一张共享的 ID 生成表,使用自增字段来生成 ID,再存到业务表主键字段中。

  2. 基于数据库(DB)的自增 ID :可以单独创建一张共享的 ID 生成表,使用自增字段来生成 ID,再存到业务表主键字段中。
    优点:

    • 实现非常简单,利用现有的数据库即可搞定;

    • ID 单调递增;

    缺点:

    • 强依赖 DB,当 DB 异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。

    • ID 发号性能瓶颈限制在单台 MySQL 的读写性能。

  3. 基于分布式协调服务: 利用 Zookeeper、Etcd 等分布式协调服务,可以实现 ID 的有序分配。虽然这种方法可以保证 ID 的顺序性,但引入了外部依赖,增加了系统的复杂度。

  4. 基于分布式缓存:使用 Redis 的 INCRBY 命令,可以为键 (Key)的数字增加指定增量。如果键不存在,则数值会被初始化为 0,然后再执行增量操作。

  5. 基于 Snowflake 算法(雪花算法): Snowflake 算法由 Twitter 开发,它结合了时间戳、机器 ID 和序列号,生成 64 位的 ID,如下图所示:

Loading

  • 1bit: 符号位(标识正负),不作使用,始终为 0,代表生成的 ID 为正数。

  • 41-bit 时间戳: 一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)

  • datacenter id + worker id (10 bits): 一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(项目中可以根据实际需求来调整)。这样就可以区分不同集群/机房的节点。

  • 12-bit 序列号: 一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。理论上 snowflake 方案的QPS约为 409.6w /s,这种分配方式可以保证在任何一个 IDC 的任何一台机器在任意毫秒内生成的 ID 都是不同的。

snowflake 雪花算法优缺点如下:

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

Leaf 介绍

Leaf 这个名字是来自德国哲学家、数学家莱布尼茨的一句话: There are no two identical leaves in the world

—— “世界上没有两片相同的树叶”

美团 Leaf 基于数据库生成以及 snowflake 雪花算法方案之上,做了进一步的优化,提供了如下两种方案:

  1. 第一种:Leaf-segment 数据库方案:

在使用数据库的方案上,做了如下改变:

原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

  1. 第二种:Leaf-snowflake 雪花算法方案:

Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

下载源码

Leaf 的 GitHub 地址是:https://github.com/Meituan-Dianping/Leaf ,如下图所示:

Loading

  • ①:leaf-core : 核心模块,包括两种方案的核心代码;
  • ②:leaf-server : 服务端工程,用于对外提供接口获取分布式 ID,以及监控页面;
  • ③:scripts : 数据库脚本;

打开命令行工具,进入到想要存放工程的文件夹下,执行如下命令,拉取 Leaf 源码。

git clone https://github.com/Meituan-Dianping/Leaf.git

准备数据库

源码拉取完毕后,还有一些前置工作。由于 Leaf-segment 方案依赖于数据库,所以还需提前将数据库、表创建好。新建一个名为 leaf 的数据库。库创建完毕后,执行如下 SQL :

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')

表设计

  • biz_tag : 用来区分业务,例如生成用户 ID、生成笔记 ID通过此标识隔离开来;

  • max_id: 表示该 biz_tag 目前所被分配的 ID 号段的最大值;

  • step: 表示每次分配的号段长度。

基于数据库生成 ID, 最原始的方案是,获取 ID 每次都需要写数据库,现在只需要把 step 设置得足够大,比如 1000。那么只有当 1000 个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了 1/step 。

表创建完成后,再插入一条业务标识为 leaf-segment-test 的记录,step 为 2000,表示号段长度为 2000, 即每次生成 2000 个 ID 。

修改配置

数据库准备好后,通过 IDE 打开 Leaf 源码工程,并编辑 leaf-server 模块 /resources 资源目录中的 leaf.properties 配置文件

将 leaf.segment.enable 配置为 true , 表示开启号段模式,并配置数据库连接等信息。

1
2
3
4
5
6
7
8
9
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/leaf?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
leaf.jdbc.username=root
leaf.jdbc.password=123456

leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

启动 Leaf

数据库连接配置完毕后,运行 leaf-server 模块下的启动类,看看能否启动成功。不出意外,你会发现控制台报错如下

Loading

因为我们目前使用的是 8.0 版本的 MySQL, 需要对 leaf-seaver 模块的 pom.xml 添加最新的驱动

1
2
3
4
5
6
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>

添加完依赖后,刷新一下 Maven。 然后,编辑 /service 包下的 SegmentService 类,在初始化数据源的时候,指定一下驱动路径,以及连接池中连接检查 SQL

1
2
3
4
5
6
         // Config dataSource
dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 省略...
dataSource.setValidationQuery("select 1");
dataSource.init();

号段模式获取分布式 ID 测试

测试一下号段模式获取分布式 ID。两种方案的接口地址,可在 LeafController 类中找到
号段模式的接口地址为
http://localhost:8080/api/segment/get/{key}

key 表示业务标识,即表中的 biz_tag 字段。比如我们想要获取 biz_tag 为 leaf-segment-test 的下一个分布式 ID, 访问接口如下
http://localhost:8080/api/segment/get/leaf-segment-test

浏览器访问,如下图所示,成功拿到了 ID 值, 并且每次刷新,都会一直递增下去

Loading

监控页

如果想获取一些监控数据,LeafMonitorController 类中定义了对应的接口,路径如下
http://localhost:8080/cache

浏览器访问,效果图如下:
Loading

Docker 安装 Zookeeper

我们已经测试了美团 Leaf-segment 号段模式(依赖数据库)来获取分布式 ID , 除了该模式外,还有 Leaf-snowflake (基于雪花算法)模式,它依赖于 Zookeeper。

Zookeeper 介绍

Apache ZooKeeper 是一个开源的分布式协调服务,用于大型分布式系统的开发和管理。它提供了一种简单而统一的方法来解决分布式应用中常见的协调问题,如命名服务、配置管理、集群管理、组服务、分布式锁、队列管理等。ZooKeeper 通过提供一种类似文件系统的结构来存储数据,并允许客户端通过简单的 API 进行读写操作,从而简化了分布式系统的复杂度。

Zookeeper 的核心特性如下:

  • 一致性:对于任何更新,所有客户端都将看到相同的数据视图。这是通过 ZooKeeper 的原子性保证的,意味着所有更新要么完全成功,要么完全失败。

  • 可靠性:一旦数据被提交,它将被持久化存储,即使在某些服务器出现故障的情况下,数据也不会丢失。

  • 实时性:ZooKeeper 支持事件通知机制,允许客户端实时接收到数据变化的通知。

  • 高可用性:ZooKeeper 通常以集群形式部署,可以容忍部分节点的故障,只要集群中超过半数的节点是可用的,ZooKeeper 就能继续提供服务。

ZooKeeper 的数据模型:

ZooKeeper 使用一个层次化的命名空间来组织数据,类似于文件系统中的目录树。每个节点(称为 znode)都可以有子节点,形成树状结构。每个 znode 可以存储一定量的数据,并且可以设置访问控制列表(ACL)来控制谁可以读取或修改数据。

ZooKeeper 的应用场景:

  • 配置管理:ZooKeeper 可以用来集中存储和管理分布式系统中的配置信息,当配置发生变化时,可以实时通知到所有客户端。

  • 命名服务:ZooKeeper 可以作为服务发现的注册中心,帮助客户端查找和定位服务。

  • 集群管理:ZooKeeper 可以用于选举主节点、检测集群成员的变化、以及监控集群的健康状况。

  • 分布式锁:ZooKeeper 提供了一种机制来实现分布式环境下的互斥访问,保证多个进程之间数据操作的正确性。

  • 队列管理:ZooKeeper 可以用来实现分布式队列,如任务调度队列或消息队列。

  1. 下载镜像

打开命令行工具,执行如下命令,拉取 Zookeeper 镜像

docker pull zookeeper:3.5.6

  1. 创建挂载文件夹

镜像下载完成后,在 E:/docker/ 目录下创建 /zookeeper 文件夹,用于存放等会启动容器时,挂载出容器内 Zookeeper 的相关配置文件,以及相关持久化数据

  1. 运行容器

执行如下命令,运行一个 Zookeeper 容器

1
docker run -d --name zookeeper -p 2181:2181 -e TZ="Asia/Shanghai" -v E:\docker\zookeeper\data:/data -v E:\docker\zookeeper\conf:/conf zookeeper:3.5.6

参数解释

  • docker run: 这是启动一个新的 Docker 容器的命令。

  • -d: 这个选项表示以守护进程模式(即后台)运行容器。

  • –name zookeeper: 给容器指定一个名字叫做 zookeeper。这可以帮助你更容易地识别和管理这个容器。

  • -p 2181:2181: 这是一个端口映射选项,它将宿主机的 2181 端口映射到容器内的 2181 端口。这意味着在宿主机上,你可以通过访问 localhost:2181 来连接到运行在容器内的 ZooKeeper 服务。

  • -e TZ=”Asia/Shanghai”: 这个环境变量设置将容器内部的时间区域设为上海时区(亚洲/上海)。这样可以确保容器内的时间与你的本地时区一致。

  • -v E:\docker\zookeeper\data:/data: 这是一个卷挂载选项,将宿主机上的 E:\docker\zookeeper\data 目录挂载到容器内的 /data 目录。通常,ZooKeeper 将数据存储在 /data 目录下,因此这个挂载点可以让你在宿主机上持久化 ZooKeeper 的数据。

  • -v E:\docker\zookeeper\conf:/conf: 类似于上面的挂载,这里将宿主机上的 E:\docker\zookeeper\conf 目录挂载到容器内的 /conf 目录。ZooKeeper 的配置文件一般位于 /conf 目录下,这样你可以在宿主机上编辑配置文件,而不会影响到容器重启后的配置。

  • zookeeper:3.5.6: 这是指定使用的 Docker 镜像,这里是 ZooKeeper 版本 3.5.6 的镜像。

  1. 容器运行成功后,可通过 docker ps 命令查看正在运行中的容器,确认一下 Zookeeper 是否启动成功了

Loading

  1. 进入 Zookeeper

执行如下命令,进入到 Zookeeper 容器中

docker exec -it zookeeper bash

接着,执行如下命令,来启动 ZooKeeper 的命令行界面(CLI),它允许用户直接与 ZooKeeper 服务器进行交互

./bin/zkCli.sh

连接成功后,效果图如下

Loading

  1. zk 基本命令

ZooKeeper CLI (zkCli) 是 ZooKeeper 分布式协调服务附带的一个命令行工具,它提供了与 ZooKeeper 服务器交互的方式。使用 zkCli,你可以执行诸如查看、创建、修改和删除 ZooKeeper 中的数据节点(znodes)的操作。

  • ls:列出当前路径下的子节点。如:查看根节点的子节点,命令如下

ls /

  • create:创建一个新的节点 (znode)

create /myNode "hello"

以上命令,将创建一个名为 /myNode 的节点,并初始化其数据为 “hello”。

Loading

  • get:获取指定节点的数据和状态信息。命令如下

get /myNode

  • set:设置指定节点的数据。命令如下

set /myNode "fresh"

  • delete:删除指定的节点(znode) 。命令如下

delete /myNode

以上命令,将删除 /myNode 节点,注意,只有当该节点没有子节点时才有效。

  • quit:退出 zkCli 命令行工具。效果如下:

Loading

美团 Leaf-snowflake 雪花算法模式测试

编辑配置
首先,编辑 leaf-server 模块中的 leaf.properties 配置文件,将 snowflake 模式开启,并配置好 Zookeeper 连接地址,如下

1
2
3
4
5
6
# 是否开启 snowflake 模式
leaf.snowflake.enable=true
# snowflake 模式下的 zk 地址
leaf.snowflake.zk.address=127.0.0.1:2181
# snowflake 模式下的服务注册端口
leaf.snowflake.port=2222

运行 Leaf
运行 leaf-server 项目,若控制台中提示 Snowflake Service Init Successfully , 则表示 Leaf-snowflake 模式初始化成功了

Loading

接口测试
项目启动成功后,访问如下接口,即可获取雪花算法 ID

/api/snowflake/get/{key}

关于参数 key , 随便填一个就行。阅读源码,查看 SnowflakeIDGenImpl 类,即可得知 key 实际并没有使用到

浏览器访问此接口,即可获取基于雪花算法生成的 ID 了,如下图所示,每次刷新结果都会不同,而且值是趋势递增的

Loading


分布式ID生成
http://bloomivy.github.io/2025/01/23/分布式ID生成/
作者
Bloom
发布于
2025年1月23日
许可协议