分布式唯一 性ID生成方法

分布式唯一 ID

背景

日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示。

比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人。简单来说,ID 就是数据的唯一标识。

一般情况下,会使用数据库的自增主键作为数据 ID。但在大数据量场景下,往往会引入分布式、分库分表等手段来应对。很明显,对数据分库分表后我们依然需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 已经无法满足需求。此时,一个能够生成全局唯一 ID 的系统是非常必要的。

业务系统对 ID 的要求

要求 说明
全局唯一性 不能出现重复的 ID 号,这是最基本的要求
趋势递增 / 单调递增 保证下一个 ID 一定大于上一个 ID
信息安全 若 ID 连续,恶意用户可按顺序扒取;若是订单号,竞对可推算日单量。部分场景需要 ID 无规则、不规则
可用性 平均延迟和 TP999 尽可能低;可用性 5 个 9;高 QPS。ID 生成系统不稳定时,订单生成等关键动作将无法执行

常见方法介绍

UUID

UUID(Universally Unique Identifier)的标准形式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符。

示例:

text 复制代码
550e8400-e29b-41d4-a716-446655440000
优缺点

优点:

  • 性能非常高:本地生成,没有网络消耗

缺点:

  • 不易于存储:UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示,很多场景不适用
  • 信息不安全:基于 MAC 地址生成 UUID 的算法可能造成 MAC 地址泄露(该漏洞曾被用于寻找梅丽莎病毒制作者位置)

ID 作为主键时,在特定环境下会存在一些问题。例如做 DB 主键时,UUID 非常不适用:

  1. MySQL 官方明确建议主键尽量越短越好,36 个字符长度的 UUID 不符合要求
  2. 对 MySQL 索引不利:若作为数据库主键,在 InnoDB 引擎下,UUID 的无序性可能引起数据位置频繁变动,严重影响性能。InnoDB 使用聚集索引,多数 RDBMS 用 B-tree 存储索引数据,主键应尽量有序以保证写入性能

雪花算法及其衍生

这是一种以划分命名空间来生成 ID 的算法(UUID 也可算作此类,因较常见单独分析)。Snowflake 是 Twitter 开源的分布式 ID 生成算法,把 64-bit 分别划分成多段,分开来标识机器、时间等。

生成规则
位段 位数 含义
第 0 位 1 符号位(标识正负),始终为 0
第 1~41 位 41 时间戳(毫秒),可支撑 (2^{41}) 毫秒(约 69 年)
第 42~52 位 10 一般前 5 位表示机房 ID,后 5 位表示机器 ID(可按项目调整),即可表示 32 个 IDC,每个 IDC 下 32 台机器
第 53~64 位 12 序列号(自增),单机每毫秒最多生成 (2^{12} = 4096) 个唯一 ID

理论上 Snowflake 方案的 QPS 约为 409.6w/s。这种分配方式可以保证:在任何一个 IDC 的任何一台机器、任意毫秒内生成的 ID 都是不同的。

优缺点

优点:

  • 毫秒数在高位,自增序列在低位,整个 ID 趋势递增
  • 不依赖数据库等第三方系统,以服务方式部署,稳定性更高,生成 ID 性能也很高
  • 可根据自身业务特性分配 bit 位,非常灵活

缺点:

  • 强依赖机器时钟:若机器时钟回拨,会导致发号重复,或服务处于不可用状态
相关实现

若项目不想自行实现唯一 ID,还可利用外部中间件,例如:

  • MongoDB ObjectId :与 Snowflake 类似,通过「时间 + 机器码 + pid + inc」共 12 个字节,以 4+3+2+3 方式最终标识成 24 长度的十六进制字符
  • Seata 内置分布式 UUID 生成器 :用于辅助生成全局事务 ID 和分支事务 ID,完整类名为 io.seata.common.util.IdWorker

数据库生成(MySQL)

1)创建数据库表
sql 复制代码
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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stub 字段无业务意义,只为占位,便于插入或修改数据;并对其创建唯一索引,保证唯一性。

2)通过 REPLACE INTO 插入数据
sql 复制代码
BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT 7558616;
COMMIT;

这里没有使用 INSERT INTO,而是使用 REPLACE INTO:

REPLACE 是 INSERT 的增强版,会先尝试插入数据:

  1. 若发现表中已有此行数据(根据主键或唯一索引判断),则先删除此行,再插入新数据
  2. 否则,直接插入新数据
优缺点

优点:

  • 非常简单,利用现有数据库系统功能实现,成本小,有 DBA 专业维护
  • ID 号单调自增,存储消耗空间小

缺点:

  • 支持的并发量不大
  • 存在数据库单点问题(可用集群解决,但增加复杂度)
  • ID 没有具体业务含义
  • 安全问题(如根据订单 ID 递增规律可推算日订单量)
  • 每次获取 ID 都要访问一次数据库(增加压力,获取也慢)
性能优化思路

可在分布式系统中多部署几台机器,每台机器设置不同初始值,且步长与机器数相等。

该架构看似能满足性能需求,但仍有以下缺点:

  1. ID 失去严格单调递增,只能趋势递增(一般业务可容忍)
  2. 数据库压力仍很大,每次获取 ID 都得读写一次数据库,只能靠堆机器提升性能
  3. 系统水平扩展方案复杂,难以实现

Redis

通过 Redis 的 INCR 命令即可实现对 ID 的原子顺序递增,例如:

bash 复制代码
INCR sequence_id_biz_type

为提高可用性和并发,可使用 Redis Cluster。

Redis 基于内存,性能很好,且生成的 ID 有序递增;但需要持久化,避免重启或故障后数据丢失。

即使开启持久化------无论是快照(RDB)、只追加文件(AOF),还是 RDB + AOF 混合持久化------仍存在丢失数据的可能,也就意味着生成的 ID 存在重复的概率。


分布式 ID 微服务

美团 Leaf 方案实现

Leaf 这个名字来自德国哲学家、数学家莱布尼茨的一句话:

There are no two identical leaves in the world

(世界上没有两片相同的树叶)

Leaf 分别在 MySQL 和雪花算法上做了相应优化,实现了两套方案:

  • Leaf-segment
  • Leaf-snowflake

Leaf-segment 数据库方案

原 MySQL 方案每次获取 ID 都得读写一次数据库,压力大。Leaf-segment 改为批量获取 :每次获取一个 segment(由 step 决定大小)号段,用完后再去数据库获取新号段,可大幅减轻数据库压力。

各业务不同的发号需求用 biz_tag 字段区分,每个 biz_tag 的 ID 获取相互隔离、互不影响。若有性能需求需要对数据库扩容,无需复杂操作,只需对 biz_tag 分库分表即可。

表设计要点
字段 说明
biz_tag 区分业务
max_id 该 biz_tag 当前已分配号段的最大值
step 每次分配的号段长度

原来每次获取 ID 都需要写数据库;现在只要把 step 设得足够大(例如 1000),只有当 1000 个号消耗完后才会重新读写一次数据库,读写频率从 1 降到 1/step。

举例说明

假设有 3 台机器,每台各取 1000 个号:

  • 第一台 Leaf 机器上是 1~1000 号段
  • 该号段用完后,会加载另一个长度为 step=1000 的号段
  • 假设另外两台号段都未更新,则第一台新加载的号段应为 3001~4000
  • 同时数据库中对应 biz_tag 的 max_id 会从 3000 更新成 4000

更新号段的 SQL:

sql 复制代码
BEGIN;
UPDATE table SET max_id = max_id + step WHERE biz_tag = xxx;
SELECT tag, max_id, step FROM table WHERE biz_tag = xxx;
COMMIT;
优缺点

优点:

  • Leaf 服务可方便线性扩展,性能可支撑大多数业务场景
  • ID 趋势递增,为 8 byte / 64 位数字,满足数据库主键要求
  • 容灾性高:Leaf 内部有号段缓存,即使 DB 宕机,短时间内仍能正常对外提供服务
  • 可自定义 max_id 大小,便于从原有 ID 方式迁移

缺点:

  1. ID 不够随机,可能泄露发号数量信息,不够安全
  2. TP999 波动大:号段用完后获取新号段时,更新数据库的 I/O 仍可能造成等待,出现偶尔尖刺
  3. DB 宕机会造成整个系统不可用

双 Buffer 优化

针对「TP999 波动」这一缺点,Leaf-segment 做了优化。

原逻辑是:号段消耗完时才取下一段。这意味着临界点的 ID 下发时间取决于下次从 DB 取回号段的时间;期间进来的请求会因号段未取回而线程阻塞。若取 DB 时网络抖动或出现慢查询,整个系统响应会变慢。

期望 DB 取号段过程无阻塞:当号段消费到某个点时,异步把下一个号段加载到内存,而不必等号段用尽。这样可大幅降低 TP999。

双 Buffer 方式:

  • Leaf 服务内部有两个号段缓存区 segment
  • 当前号段已下发 10% 时,若下一个号段未更新,则另启更新线程去更新下一个号段
  • 当前号段全部下发完后,若下个号段已准备好,则切换为当前 segment 继续下发,循环往复

建议: segment 长度设置为服务高峰期发号 QPS 的 600 倍(约 10 分钟)。这样即使 DB 宕机,Leaf 仍能持续发号 10~20 分钟。每次请求都会判断下个号段状态并更新,因此偶尔的网络抖动一般不影响下个号段更新。


Leaf 高可用容灾

针对「DB 可用性」问题,可采用一主两从,同时分机房部署;Master 与 Slave 之间采用半同步方式同步数据。

美团内部使用了奇虎 360 的 Atlas 数据库中间件(已开源,改名为 DBProxy)做主从切换。

该方案在某些情况下会退化成异步模式,极端情况下仍可能造成数据不一致,但概率非常小。若要保证 100% 强一致,可选择「类 Paxos 算法」实现的强一致 MySQL 方案,如 MySQL 5.7 的 MySQL Group Replication;运维成本和精力会相应增加,按实际情况选型即可。


Leaf-snowflake 方案

背景

Leaf-segment 可生成趋势递增的 ID,同时 ID 号可计算,不适用于订单 ID 等场景。例如竞对在两天中午 12 点分别下单,通过订单 ID 相减就能大致推算公司一天的订单量,这是不能忍受的。面对这一问题,美团提供了 Leaf-snowflake 方案。

Leaf-snowflake 完全沿用 Snowflake 的 bit 位设计,即 1 + 41 + 10 + 12 方式组装 ID。

对于 workerID 的分配:

  • 服务集群较小:可完全手动配置
  • Leaf 服务规模较大:手动配置成本太高,因此使用 Zookeeper 持久顺序节点 自动为 snowflake 节点配置 workerID
启动步骤
  1. 启动 Leaf-snowflake 服务,连接 Zookeeper,在 leaf_forever 父节点下检查自己是否已注册(是否有该顺序子节点)
  2. 若已注册 :直接取回自己的 workerID(ZK 顺序节点生成的 int 类型 ID),启动服务
  3. 若未注册 :在该父节点下创建持久顺序节点,创建成功后取回顺序号作为自己的 workerID,启动服务
弱依赖 ZooKeeper

除了每次去 ZK 拿数据外,也会在本机文件系统上缓存一个 workerID 文件。当 ZooKeeper 出现问题,且机器恰好需要重启时,仍能保证服务正常启动,从而做到对第三方组件的弱依赖。

解决时钟问题

该方案依赖时间。若机器时钟发生回拨,可能生成重复 ID,因此需要解决时钟回退问题。

启动时检查:

  1. 新节点 :通过对比其余 Leaf 节点系统时间判断自身时间是否准确------取所有运行中 Leaf-snowflake 节点的 IP:Port,经 RPC 得到各节点系统时间,计算 sum(time) / nodeSize,看本机时间与该平均值是否在阈值内:
    • 准确 → 正常启动
    • 不准确 → 认为本机系统时间发生大步长偏移,启动失败并报警
  2. 已在 ZooKeeper 登记的老节点:同样比较自身系统时间、ZooKeeper 上本节点曾记录时间,以及所有运行中 Leaf-snowflake 节点时间;不准确同样启动失败并报警

另外,运行过程中每隔一段时间,节点都会上报自身系统时间写入 ZooKeeper。

服务运行中,机器 NTP 同步也可能造成秒级回退;由于强依赖时钟,对时间较敏感。

美团建议的三种解决方案:

  1. 直接关闭 NTP 同步
  2. 时钟回拨时不提供服务,直接返回 ERROR_CODE,等时钟追上即可
  3. 做一层重试并上报报警系统;或发现时钟回拨后自动摘除本节点并报警

从美团实际运行情况看,在 2017 年闰秒那一次曾出现部分机器回拨;由于 Leaf-snowflake 的策略保证,成功避免了对业务造成影响。


美团 Leaf 现状

Leaf 在美团点评内部覆盖金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。

指标 数据
压测机器 4C8G
QPS 近 5 万/s
TP999 1ms
日调用量 亿级

已能够满足大部分业务需求。

相关推荐
小蒜学长1 小时前
微信小程序-高校自习室管理系统(代码+数据库+LW)
java·spring boot·后端·微信小程序·自习室管理
M1A11 小时前
告别代码风格之争:在 VS Code 中用 Black 一键格式化 Python
后端
IT_陈寒1 小时前
Python多进程共享变量这个坑,差点让我加班到凌晨
前端·人工智能·后端
撸串-研究生2 小时前
springboot福利院管理系统73979
java·spring boot·后端·福利院管理系统
xixiaoyunya2 小时前
别再写一堆 if-else 了:用策略模式重构你的业务代码
后端
tachibana22 小时前
Go 语言里的 Context 是什么?
开发语言·后端·golang·go·语言
鶴哥只手遮天3 小时前
Spring Framework 7.0.9 深度剖析-05-配置元数据解析:XML、注解与 JavaConfig
java·后端·开源
鶴哥只手遮天3 小时前
Spring Framework 7.0.9 深度剖析-16-异步与调度
java·后端·开源
高频因子挖掘机3 小时前
500只股票 × 多年日线:数据获取与本地存储如何分工?
后端·github·api