当前位置: 首页 > news >正文

分布式 ID 哪家强?吃透雪花不迷茫!

本篇我们会介绍一个重要的通用基础组件:唯一ID生成器。此组件的应用范围相当广泛,比如淘宝、京东上的用户ID、商品ID、订单号......都需要唯一ID生成器,因为这些都是需要唯一标识的。

很多人项目里随手用 UUID、数据库自增、Redis 自增,但你真的想过它们的坑吗?今天我们从传统方案对比入手,再深入讲透雪花算法,最后聊聊大厂成熟落地方案美团 Leaf,帮你彻底搞懂分布式 ID 该怎么选、怎么用。

兄弟姐妹们点点赞点点关注谢谢。

一、分布式ID到底需要什么?

我认为一个合格的分布式 ID 生成器,至少要满足这几点:

  1. 全局唯一:最基础要求,多节点、多库绝对不能重复;

  2. 高性能:高并发场景下不能成为瓶颈;

  3. 有序友好:分为单调递增趋势递增,数据库主键优先趋势递增,利于索引;

  4. 高可用:宕机、网络抖动、时间异常不崩;

  5. 无安全泄露:不能轻易通过 ID 推算出用户量、订单量。

注意第三点,这里特别区分两个高频概念:

  • 单调递增:ID 严格依次变大,比如 1,2,3,4,多节点很难实现;

  • 趋势递增:整体随时间变大,局部允许乱序,雪花算法就是典型,分布式首选

二、传统分布式id方案

2.1 UUID

JDK1.5在语言层面实现了UUID,一行代码UUID.randomUUID()就能轻松生成全球唯一ID,本地生成、无网络依赖,很香。

import java.util.UUID; public class idgenerator{ public static void main(String[] args){ String uuid = UUID.randomUUID().toString(); System.out.println(uuid); } }

但它最大的问题:占用空间大、无序、字符串格式

下面来详细讲一下这三点:

1. 占用空间大

UUID长这样,4aaa4aaa-5a5a-66ss-44ss-a5a5a565aa55,4个 “-” 连接字符,32个十六进制数字,共36个字符。

但是,

B+ 树节点容量降低: 数据库(如 MySQL InnoDB)的主键索引使用的是 B+ 树,其每个节点(页)的大小是固定的(默认 16KB)。主键的体积越大,一个节点能容纳的主键数量就越少。这会导致 B+ 树的层级变深,查询时需要的磁盘 I/O 次数增加,直接拖慢查询速度。

二级索引体积膨胀: 在 InnoDB 中,辅助索引(非主键索引)的叶子节点存储的是主键的值。这意味着,如果你有一个 36 字节的 UUID 作为主键,那么你表里的每一个二级索引都会多出极大的存储开销。表越大、索引越多,浪费的磁盘和内存空间就越惊人。相比之下,BIGINT只需要 8 个字节。

2. 无序

UUID生成的ID是无序的,没有任何递增规律。然而, InnoDB 主键使用的是聚簇索引,这意味着数据在物理存储上是按照主键的顺序依次排列的。如果主键是自增有序的,每次插入新数据,数据库只需在当前数据页的末尾“追加”即可,效率极高。但如果主键是无序的随机 UUID,数据库在插入新数据时,往往需要将其强行塞入到之前已经写满(或半满)的数据页中间。为了腾出空间,数据库不得不把满页的数据拆分成两个页,这就是页分裂(Page Split)。大量的页分裂会导致频繁的磁盘数据移动、产生大量内存碎片,并极大地降低插入(Insert)的并发性能。

3. 字符串格式

在实际业务开发中,为了方便调试和展示,开发人员通常会将 UUID 直接以VARCHAR(36)CHAR(36)的字符串格式存储在数据库中。

这样会导致:

  • 比较效率极低:在数据库执行查询、排序或多表 Join 时,底层需要频繁比对主键的值。CPU 对整型(如BIGINT)的比较是原生的指令,速度极快;而对字符串的比较,需要逐个字符进行查对。

  • 字符集开销:字符串处理还会受到数据库字符集(如utf8mb4)和排序规则(Collation)的影响,这又增加了一层额外的计算开销。在千万级的数据表里,字符串比对带来的性能损耗会被无限放大。

2.2 数据库自增ID

如果是单体架构,数据库自增 ID 肯定是非常香的:它完美避开了 UUID 的所有缺点(体积小、绝对有序、整型比较极快)。但在如今的高并发、分布式场景下,它不适合。

下面来讲一下为什么:

1.性能瓶颈与单点故障

在分布式系统中,你的应用服务可以部署几十上百个节点来抗高并发,但如果全都依赖同一个数据库实例来生成自增 ID,那么这个数据库就成了全村唯一的“独苗”。

  • 写并发瓶颈:所有的写请求最终都要排队去这一个数据库里拿 ID。不管你前置服务有多牛,一旦并发量上来(比如双十一秒杀),数据库的写 TPS(每秒事务处理量)上限就是你整个系统的性能天花板。
  • 单点宕机风险:如果这台生成 ID 的主库宕机了,整个系统将无法插入任何新数据,直接陷入瘫痪。虽然可以做主从复制,但主从切换期间依然会导致服务不可用,且可能引发 ID 重复(主库刚发了一个 ID 还没同步到从库就挂了)。

2. 分库分表时的冲突

当单表数据量达到千万级(比如订单表),我们就必须进行分库分表操作。假设你把一张表拆成了table_0table_1两个物理表,如果继续用原生的自增 ID,那就不好了。几个例子:

table_0的第一条数据 ID 是 1,table_1的第一条数据 ID 也是 1。当你把这两张表的数据聚合到一起查询,或者同步到数据仓库时,完全无法区分这两条数据谁是谁。

3. 商业机密泄露

因为自增 ID 具有极强的规律性和可预测性,所以如果你的电商订单号是自增的,竞争对手只要今天早上下一单(发现订单号是 10000),晚上再下一单(发现订单号是 15000),就能精准算出你今天的真实日单量是 5000 单,这跟裸奔有什么区别,商业机密全泄露了。

2.3 Redis INCRBY 命令

Redis提供的 INCRBY 命令可以为键(Key)的数字值加上指定的增量(Increment)。

INCRBY key increment命令的作用是:将key中储存的数字值增加指定的步长(increment)。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行INCRBY命令。

因为 Redis 单线程处理的机制,无论有多少台应用服务器同时向 Redis 发起取号请求,Redis 都会排好队依次执行,绝对不会返回相同的 ID。

简单代码演示:

Spring Boot中,我们通常会借助StringRedisTemplate来实现。

基础玩法:每次请求自增 1

这种方式相当于把 Redis 当作一个分布式的、极其快速的数据库AUTO_INCREMENT来用。

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisIdGenerator { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 生成业务 ID * @param businessKey 业务标识,例如 "order" * @return 全局唯一的递增 ID */ public long generateId(String businessKey) { String key = "id:" + businessKey; // opsForValue().increment 就是底层调用 INCRBY 命令 // 每次增加 1 return stringRedisTemplate.opsForValue().increment(key, 1); } }import org.springframework.beans.factory.annotation.Autowired;

进阶玩法:号段模式(批量获取,性能翻倍)

每次生成一个 ID 都去请求一次 Redis,依然存在网络开销。高并发场景下,应用服务器可以一次性向 Redis 申请一批 ID(比如 1000 个),存放在本地内存中,本地发完之后再去请求 Redis。

/** * 号段模式:一次性获取一个步长的 ID 最大值 */ public long getNextIdBatch() { String key = "id:order:segment"; long step = 1000L; // 步长设置为 1000 // 比如原本是 0,执行后 maxId 变成 1000 Long maxId = stringRedisTemplate.opsForValue().increment(key, step); // 应用拿到 maxId 后,就可以知道当前应用可用的 ID 范围是: [maxId - step + 1, maxId] // 即 [1, 1000]。这段 ID 可以在应用本地直接分配,无需再通过网络请求 Redis return maxId; }

那来总结一下Redis方案的优缺点吧

  1. 优点:

  • 性能极高:纯内存操作,能抗高并发,配合集群性能更高。

  • 生成的 ID 趋势递增:纯数字,且整体呈上升趋势,对数据库的 B+ 树索引极其友好,避免了页分裂。

  • 支持灵活配置:可以按天归零(比如生成2026052200001这种带日期的流水号),也可以结合号段模式减少网络压力。

  1. 缺点

  • 严重依赖网络:如果不使用号段模式,每次获取 ID 都要经历完整的网络请求(应用 -> Redis -> 应用)。

  • 宕机数据丢失导致 ID 冲突(最大隐患):Redis 是内存数据库,它的持久化机制(RDB 快照或 AOF 每秒刷盘)都存在微小的时间差。

2.4 小结

那来总结一下这三位,

UUID伤硬盘,

数据库扛不住,

Redis又有网络开销和宕机回退的风险。

为了追求绝对的高性能(本地生成,0 网络延迟)和高可用(不依赖独立中间件组件宕机),下面有请

雪花算法(Snowflake)

隆重登场。

三、雪花算法(Snowflake)

3.1 雪花算法的 64 位核心结构

0 | 41 位时间戳 | 10 位机器 ID | 12 位序列号

1 bit:符号位(不用)

  • 在 Java 中,最高位是符号位。因为我们要生成的 ID 总是正数,所以这一位固定为0

41 bit:时间戳(精确到毫秒

  • 用来记录当前时间距离某个初始时间(可以是上线时间)的毫秒差值。

  • 41 个二进制位最大能表示 $$2^{41}-$$ 毫秒,换算下来大约可以使用69 年。这也是为什么雪花生成的 ID 是“趋势递增”的,因为时间总是在往前走。

10 bit:机器/工作节点 ID

  • 用来标识当前是哪一台服务器在生成 ID。

  • 10 个二进制位最大能表示 $$2^{10} = 102$$ 台机器。在实际部署中,通常会拆分为 5位机房 ID(数据中心) + 5位机器 ID,确保不同的机器生成的 ID 绝对不会重复。

12 bit:序列号

  • 用来处理毫秒内的并发碰撞。如果同一台机器在同 1 毫秒内收到了多个获取 ID 的请求,就通过递增这个序列号来区分。

  • 12 个二进制位最大能表示 $$2^{12} = 409$$。意味着单台机器 1 毫秒内最多可以生成4096个不同的 ID(单机 QPS 理论峰值可达 410万级别)。

最终效果是,Snowflake算法给出的唯一 ID生成器是一个支持多机房共1024个服务实例规模、单个服务实例每秒可生成410万个long类型唯一 ID的分布式系统,且此系统可以正常工作69年。

3.2 为什么大厂都爱用它?

  • 极简且高性能:没有任何复杂的中间件依赖,纯内存位运算,快到飞起。

  • 自带时序性:41 位时间戳在最前面,保证了 ID 整体是随着时间递增的,这对于 MySQL B+ 树索引来说简直是“神仙数据”,写入速度极快,不会频繁发生页分裂。

  • 绝对安全可控:完全不依赖网络,不存在网络超时或单点故障问题。

不过,雪花算法也有一些问题:机器ID重复时钟回拨问题。那是怎么解决的呢?

3.3 机器ID重复与解决方案

雪花算法的 64 位中,有 10 位是分给机器的(最多支持 1024 台机器)。 如果你的服务集群里,有两台机器(比如 Node A 和 Node B)拿到了相同的机器 ID,那么当它们在同一个毫秒内接收到并发请求时,极大概率会生成完全一模一样的序列号。最终结果就是:两台机器算出了完全相同的 64 位 ID,直接导致数据库主键冲突,服务大面积报错。

本篇要讲的解决方案

ZooKeeper / Etcd 注册中心(美团 Leaf 等主流首选)

机器启动时,连接 ZK,在一个指定的目录下创建一个临时顺序节点。ZK 会自动为这个节点追加一个递增的序号,应用提取这个序号作为自己的机器 ID。 这是极其可靠的,解决了容器化动态扩缩容带来的分配难题,同时 ZK 还能兼职做时钟回拨的校验中心。

3.4 时钟回拨问题与解决方案

既然雪花算法强依赖于机器的本地时钟,那么机器的时间真的绝对可靠吗?这就引出了雪花算法最经典、也是最致命的架构隐患——时钟回拨

1. 问题来源

计算机的时间并非凭空而来,而是靠主板上的石英晶体振荡器和纽扣电池来模拟的。通常这块晶体的振动频率为 32,768Hz(即每振动 32,768 次代表 1 秒过去)。 但物理硬件并不完美,受极端环境(如低温)影响,晶体振动会出现误差。在正常情况下,服务器每天也会产生±1s左右的计时误差,这种现象被称为“时钟漂移”。

2. NTP 同步导致“时间倒流”

既然单台机器的时间会飘,集群环境怎么保证时间一致?1985 年,David L. Mills 设计了NTP网络时间协议,它可以将局域网内的机器时钟误差强行拉平到 1ms 以内。 但 NTP 的介入带来了一个极其恐怖的副作用:如果某台机器的时钟跑得太快了,NTP 会毫不留情地把它的时间往回拨!

打个比方: 假设唯一的 ID 生成服务在2023年1月1日 11:05:05正常发号。此时 NTP 触发时间校准,发现机器时间快了,直接将其拽回到了11:05:00。 当新的请求打过来时,雪花算法拿到的时间戳又回到了 5 秒前。此时生成的 ID,将与 5 秒前生成的 ID完全重复,瞬间引爆数据库主键冲突!

3. 解决方案

为了防范这种灾难,ID 生成器在每次发号时,都必须把当前的时间戳存下来(记作svr.millisPassed)。当新请求到来时,计算最新的时间戳millis进行比对:一旦发现millis < svr.millisPassed,就说明时间发生了倒流,触发了时钟回拨!

面对时钟回拨,通常有以下三种处理策略:

  • 解法一:短回拨 -> 阻塞等待(Sleep)如果回拨的时间跨度极短(比如只有 10ms),最简单优雅的做法就是让当前处理请求的线程原地阻塞等待一会儿。等物理时间真实地流逝,追平了上次记录的发号时间后,再重新执行生成逻辑。

  • 解法二:长回拨 -> 强硬拒绝(抛异常)如果时间一口气回退了几秒甚至更长,阻塞等待会让海量请求瞬间堆积,压垮内存。此时必须当机立断,直接拒绝发号请求(抛出运行时异常),将流量路由给集群里的其他健康节点。

  • 解法三:釜底抽薪 -> 关闭NTP同步一种更为极致的做法是,让所有充当 ID 生成器的服务实例彻底关闭 NTP 时间同步功能,从物理层面上杜绝“时间倒流”的可能。 当然,这会导致部分节点的“时钟漂移”越来越严重。配套的兜底方案是:引入外部监控,一旦发现某台服务实例的漂移幅度超过了设定的阈值,直接将其从服务集群中人工或自动摘除。

3.5 最终架构

基于 Snowflake 算法的唯一 ID 生成器服务的最终架构如下:

  • 每个机房都单独编号。

  • 在每个机房内都部署一个 worker ID 分配器,可以基于数据库或 etcd 等。

  • 在每个机房内都部署若干唯一 ID 生成器服务实例,这些服务实例每次启动时都从本机房的 worker ID 分配器中获取 worker ID。

  • 每个服务实例都在本地维护当前毫秒时间戳和毫秒内并发数,并与机房编号、worker ID 一起作为输入参数,执行 Snowflake 算法生成唯一 ID。

四、美团点评开源方案:Leaf

Leaf是美团开源的工业级分布式ID 生成器,支撑美团外卖、支付、金融等核心高并发业务。它提供两套互补方案:

  • Leaf-segment 号段模式(严格单调递增)

  • Leaf-snowflake 雪花增强模式(高并发安全、防时钟回拨)

4.1 Leaf-segment 号段模式(数据库号段方案)

4.1.1 核心思想

不再逐条向数据库申请 ID,而是一次性批量拉取一大段 ID 缓存到本地内存,业务直接从内存取号,极大降低数据库压力。

4.1.2 号段数据表设计

表格

字段名类型说明
biz_tagvarchar业务唯一标识(隔离不同业务 ID)
max_idbigint当前已分配的最大 ID
stepint每次批量拉取的号段步长
descvarchar业务描述
update_timetimestamp更新时间

4.1.3 执行流程示例

假设:biz_tag = order,max_id = 10000,step = 2000

  1. Leaf 执行事务更新:max_id = max_id + step

  2. 本次拿到可用号段:10001 ~ 12000

  3. 后续请求直接从内存取号,无需访问 DB

step 越大,DB QPS 越低,性能越好。

4.1.4 原生方案缺陷

  • 号段耗尽瞬间需要同步查库

  • 数据库抖动、慢查询会阻塞业务请求

4.1.5 Leaf 核心优化:双缓存 + 异步预加载

优化逻辑

  1. 内部维护主、备两个号段缓冲区

  2. 当前号段消耗10%阈值,异步提前加载新号段

  3. 老号段用尽,直接切备用缓存,业务零阻塞

4.1.6 优缺点总结

✅ 优点

  • 严格单调递增

  • 无时间戳依赖、无时钟回拨问题

  • 性能极高、支持超大并发

  • 多业务 Tag 隔离,互不影响

❌ 缺点

  • ID 连续递增,会暴露业务订单体量

  • 服务重启会浪费当前未用完的号段

4.2 Leaf-snowflake 雪花增强模式

为了解决号段模式泄露业务量的问题,Leaf 基于原生雪花做了生产级增强,彻底搞定两大痛点:机器 ID 重复、时钟回拨

4.2.1 ID 结构(完全兼容雪花)

0 | 41位时间戳 | 10位机器ID | 12位序列号

4.2.2 机器 ID 自动分配(基于 ZK)

原生雪花最大痛点:机器 ID 手动配置,极易重复。

Leaf 采用ZK持久顺序节点自动分配 WorkerId:

流程

  1. Leaf 启动自动在 ZK 创建持久顺序节点

  2. ZK 自增序号作为当前实例 WorkerId

  3. 本地文件缓存 WorkerId,ZK宕机也可复用

  4. 彻底杜绝机器 ID 重复

4.2.3 时钟回拨解决方案(业界最优)

校验机制

  1. 启动校验:本机时间 < 历史最大时间 → 直接启动失败,拒绝服务

  2. 运行监控:每 3s 上报时间戳,持续检测时间回拨

  3. 集群时间比对:参考集群平均时间,识别时间漂移

配合服务器关闭 NTP 自动对时,可彻底消灭生产时钟回拨问题

4.3 Leaf 两种方案选型对比

表格

方案递增特性泄露业务量时钟回拨风险适用场景
Leaf-segment严格单调递增会泄露日志、流水、内部业务
Leaf-snowflake趋势递增安全不泄露几乎为 0订单、支付、用户 ID 核心业务

4.4 小结

  1. Leaf-segment:高性能、稳、无时钟问题,适合内部非敏感业务

  2. Leaf-snowflake:优化版雪花算法,解决原生所有生产坑点,是互联网高并发核心业务首选

  3. 企业级开发无需手写雪花算法,直接接入 Leaf 即可稳定落地

http://www.jsqmd.com/news/872910/

相关文章:

  • 2026年降AI工具输出格式横评:五款主流工具Word格式保留完整程度对比报告 - 还在做实验的师兄
  • 金刚砂地坪技术选型指南及东北合规厂家实测解析 - 奔跑123
  • 温州6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 一站式管理多个项目API Key与访问权限的最佳实践
  • 麦阵波束算法——MVDR
  • 湛江6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 滁州6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 在Taotoken平台试用不同模型后,关于输出质量与风格差异的初步印象
  • AI赋能竞对分析:告别低效人工,抢占先机
  • 终极CompreFace人脸识别模型实战指南:5大场景选型与部署方案
  • 泰州6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 2026年B2B制造业GEO优化服务商推荐:工业品牌AI搜索可见度提升实战指南
  • 使用Taotoken的OpenAI兼容协议与PythonSDK三分钟完成大模型接入
  • 材料科学论文降AI工具免费推荐:2026年材料科学毕业论文AIGC超标免费4.8元达标完整方案 - 还在做实验的师兄
  • 绍兴6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 昇腾NPU 的“后厨五人组“:CANN 架构原理一把抓
  • PolarDB-X 存储引擎核心技术 | 索引前缀压缩 - Prefix Compression
  • 宿迁6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • i茅台自动预约系统:轻松搞定茅台抢购的终极解决方案 [特殊字符]
  • 硬件加密为何成为工业级SSD的数据安全底座?天硕存储构建自主可控安全体系
  • 金华6月雨季来临,房屋漏水怎么办?卫生间免砸砖防水、外墙、屋面+地下室渗漏。权威防水公司靠谱TOP5推荐(2026年6月本地最新深度调研) - 企业资讯
  • 2026大模型技术全景:从“写代码“到“做工程“
  • 跨越嘈杂车间的无障碍沟通:工业降噪与特种方言识别在智能巡检中的声学优化实践
  • taotoken多模型聚合api如何助力个人开发者降本增效
  • 王小川All in医疗大模型:从通用赛道抽身,“造AI医生”能否突围?
  • 选国内轻奢潮鞋,跟着明星上脚款准没错
  • Day5学习--SpringBoot详解
  • 在Linux中运行Windows的exe程序
  • XZ62N,0.7uA静态电流,NMOS输出电压检测芯片
  • 排水管网可视化管理平台,免巡检、故障快速定位