【分布式组件雪花ID】

作者:老友記日期:2026/2/26

分布式组件雪花ID

    • 组成
    • 时钟回拨解决方案汇总
      • 方案一:等待后重试(阻塞等待)
        • 方案二:预留回拨位(占用序列号位)
          • 1. "预留回拨位"的核心思想
            * 2. 位分配对比图
            * 3. 具体工作场景模拟
            * * 正常情况(时间向前走):
            * 发生时钟回拨(时间从1000跳回999):
            * 4. 这种方案的优缺点
            * 5. 位运算代码示意(Java)
        • 方案三:采用"未生成ID最大上限"自动漂移
        • 方案四:外部存储兜底(依赖Redis/ZooKeeper)

组成

雪花ID(Snowflake ID)的生成规则,核心是将一个64位的整数(long型)按二进制位划分成几个部分,每个部分代表不同的含义,以此来保证在分布式系统中的唯一性和有序性。

下面是它的标准位分配规则:

  • 1位符号位:这是最高位,在二进制中固定为 0。这样做是为了确保最终生成的ID是一个正整数(如果为1,则结果为负数)。
  • 41位时间戳:记录生成ID时的毫秒级时间戳。通常不是一个绝对时间,而是从一个自定义的"纪元"(起始时间)开始计算的时间差值。41位的时间戳足够使用大约 69年 而不重复。
  • 10位机器标识:用于区分分布式系统中的不同节点,最多可以支持 1024 台机器(或进程)同时生成ID而不冲突。这10位在实际应用中通常可以再拆分为两部分:
    • 数据中心ID (5位):最多支持 32 个数据中心。
    • 机器/进程ID (5位):每个数据中心最多支持 32 台机器。
  • 12位序列号:这是解决高并发冲突的关键。当同一毫秒内在同一台机器上生成多个ID时,这个序列号会从0开始递增。12位的序列号允许每台机器每毫秒最多生成 4096 个不同的ID。如果同一毫秒内生成的ID数量超过4096个,生成器会等待到下一毫秒再继续生成。

你可以把整个ID想象成一个按位拼接起来的结构:
(0) | (41位时间戳差值) | (5位数据中心ID) | (5位机器ID) | (12位序列号)

额外需要注意的几点:

  • 趋势递增:由于ID的高位部分包含时间戳,因此生成的ID在整体上是随着时间递增的,这对数据库索引很友好。
  • 时钟回拨问题:该算法强依赖于服务器时间的正确性。如果系统时间发生了回拨(例如NTP同步导致),可能会导致生成重复的ID。标准的实现中,如果检测到当前时间戳小于上一次生成ID的时间戳,通常会抛出异常或阻塞等待。

希望这份拆解能帮你理清雪花ID的生成逻辑。如果你对其中提到的“时间回拨问题”的解决方案,或者不同编程语言的实现细节感兴趣,我们可以继续探讨。

时钟回拨解决方案汇总

方案一:等待后重试(阻塞等待)

既然回拨的时间通常很短(往往是NTP同步导致的几毫秒回拨),让线程等待一下是比较直接的做法。

核心逻辑:如果当前时间戳小于上次生成ID的时间戳,就计算需要等待的时间,然后让线程sleep

1// 伪代码示例
2public synchronized long nextId() {
3    long currentTimestamp = System.currentTimeMillis();
4    
5    if (currentTimestamp < lastTimestamp) {
6        // 计算需要等待的时间(加上一点缓冲,比如5ms)
7        long offset = lastTimestamp - currentTimestamp;
8        if (offset <= 5) {
9            try {
10                // 等待差值时间
11                Thread.sleep(offset);
12                // 睡醒后重新获取时间
13                currentTimestamp = System.currentTimeMillis();
14                
15                // 如果时间依然小于上次记录,说明回拨时间较长,抛异常
16                if (currentTimestamp < lastTimestamp) {
17                    throw new RuntimeException("时钟回拨严重,等待后仍无法恢复");
18                }
19            } catch (InterruptedException e) {
20                throw new RuntimeException("等待时钟恢复被中断");
21            }
22        } else {
23            throw new RuntimeException("时钟回拨时间太长,无法通过等待解决");
24        }
25    }
26    
27    // ... 后续序列号处理逻辑
28    lastTimestamp = currentTimestamp;
29}
30

适用场景:适用于回拨时间较短(<10ms)且对RT(响应时间)不敏感的业务。

方案二:预留回拨位(占用序列号位)

这是一种比较巧妙的设计,借鉴了百UidGenerator和美团Leaf的思路。既然回拨是不可避免的,干脆在ID生成逻辑里给它留一个位置。

核心逻辑
12位序列号拆分成两部分,比如:

  • 5位作为回拨标志位或预留位
  • 7位作为真正的序列号

当发生时钟回拨时,我们不去等待时间,而是允许在"过去的时间戳"上继续生成ID,但通过修改回拨标志位来确保唯一性。

注意:这种方案会降低单机QPS(从原本的4096/ms降到128/ms左右),用并发性能换可用性。

1. "预留回拨位"的核心思想

"预留回拨位"的思路是:既然我无法阻止时间回拨,那我就在ID里留出几个比特位,专门用来标记"这是回拨期间生成的ID"

这样一来,即使时间戳和机器码都一样,只要这个"回拨标志"不同,ID就是唯一的。

2. 位分配对比图

假设我们改造一下标准的位分配(标准是1+41+10+12):

标准雪花ID(64位):

1┌─────────────────┬──────────────────────┬──────────────────┬─────────────────┐
2   1位符号位(0)     41位时间戳             10位机器ID         12位序列号     
3                                                            (0-4095)      
4└─────────────────┴──────────────────────┴──────────────────┴─────────────────┘
5

改造后(预留回拨位):

1┌─────────────────┬──────────────────────┬──────────────────┬────────┬─────────┐
2   1位符号位(0)     41位时间戳             10位机器ID       5位回拨  7位序列号 
3                                                          标志位          
4└─────────────────┴──────────────────────┴──────────────────┴────────┴─────────┘
5

看到变化了吗?我们把原来的12位序列号,拆成了两部分:

  • 高位部分(5位):叫做"回拨标志位"或者"预留位"。
  • 低位部分(7位):真正的序列号(0-127)。
3. 具体工作场景模拟

假设某台机器的10位机器ID0000000001(二进制)。

正常情况(时间向前走):
  • 当前时间戳 T1 = 1000
  • 预留回拨位 = 00000 (正常情况下全为0)
  • 序列号从0开始递增:0000000, 0000001, 0000010
  • 生成的ID就是: (时间戳T1) + (机器ID) + 00000 + 序列号
发生时钟回拨(时间从1000跳回999):

系统检测到当前时间戳 999 小于上次的 1000

传统做法:抛异常或等待。
预留回拨位做法

  1. 不等待:直接使用上次的时间戳 1000 来生成ID。
  2. 修改标志位:把预留的5位回拨位,从 00000 改成 00001
  3. 序列号归零:从 0000000 开始重新计数。

这样一来,虽然这一批ID和上一批ID的时间戳相同、机器码相同,但预留回拨位不同,所以它们在二进制层面是完全不同的数字,保证了唯一性。

如果回拨持续,或者同一毫秒内序列号用完了(7位只能支持128个/ms),还可以继续递增预留位:

  • 第一批(正常):00000 + 序列号
  • 第二批(回拨1次):00001 + 序列号
  • 第三批(回拨2次):00010 + 序列号

  • 直到预留位用完(32种可能),才真正无法继续生成。
4. 这种方案的优缺点

优点

  • 零等待:发生回拨时,业务线程完全不用阻塞,性能不受影响。
  • 可用性高:只要回拨次数不超过预留位的容量(比如5位最多支持32次),系统都能正常运行。

缺点

  • QPS下降:原本每毫秒能生成4096个ID,现在只能生成128个(7位序列号)。对于绝大多数单体应用,128个/ms已经够用(相当于12.8万 QPS),但如果你的接口流量特别大,这个方案就不太合适。
  • ID趋势递增特性变弱:由于回拨期间时间戳是"停滞"的,ID的增长曲线会出现短暂的平台期,而不是严格的时间递增。
5. 位运算代码示意(Java)

为了让你更有体感,这里是一段极简的位运算示意代码:

1public class SnowflakeWithReservedBit {
2    // 假设机器ID是10位,这里就不展开了
3    private long machineId = 1L;
4    
5    // 位分配
6    private final long timestampBits = 41L;
7    private final long machineIdBits = 10L;
8    private final long reservedBits = 5L;  // 预留回拨位
9    private final long sequenceBits = 7L;   // 真正的序列号
10    
11    // 上次生成的时间戳
12    private long lastTimestamp = -1L;
13    // 预留位值 (正常情况下为0)
14    private long reserved = 0L;
15    // 序列号
16    private long sequence = 0L;
17    
18    public synchronized long nextId() {
19        long currentTimestamp = System.currentTimeMillis();
20        
21        // 发生时钟回拨
22        if (currentTimestamp < lastTimestamp) {
23            // 核心:不改变时间戳,只递增预留位
24            reserved++;
25            // 如果预留位溢出(超过了5位的最大值31),说明回拨次数太多,真的扛不住了
26            if (reserved > (1 << reservedBits) - 1) {
27                throw new RuntimeException("回拨次数过多,预留位已用完");
28            }
29            // 序列号重新从0开始
30            sequence = 0L;
31        } 
32        // 同一毫秒内
33        else if (currentTimestamp == lastTimestamp) {
34            // 正常的序列号递增
35            sequence++;
36            // 如果7位序列号用完了(>127),等待下一毫秒
37            if (sequence > (1 << sequenceBits) - 1) {
38                // 自旋等待下一毫秒
39                while (System.currentTimeMillis() <= lastTimestamp) {
40                    // busy wait
41                }
42                // 进入下一毫秒后,序列号和预留位都要重置
43                sequence = 0L;
44                reserved = 0L; // 预留位归零,因为时间前进了
45                currentTimestamp = System.currentTimeMillis();
46            }
47        } 
48        // 时间正常向前走
49        else {
50            sequence = 0L;
51            reserved = 0L; // 时间正常,预留位归零
52        }
53        
54        lastTimestamp = currentTimestamp;
55        
56        // 位运算组装(这里简化了移位操作)
57        return (currentTimestamp << (machineIdBits + reservedBits + sequenceBits))
58                | (machineId << (reservedBits + sequenceBits))
59                | (reserved << sequenceBits)
60                | sequence;
61    }
62}
63

看完这个例子,你对"预留回拨位"的机制是不是更清晰了?如果有具体的代码细节想讨论,可以再告诉我。

方案三:采用"未生成ID最大上限"自动漂移

这种方案是当检测到时钟回拨时,不抛异常,而是在上次生成ID的时间戳上继续生成,直到当前系统时间追上那个时间戳。

核心逻辑

1// 如果当前时间小于上次时间
2if (currentTimestamp < lastTimestamp) {
3    // 直接使用上次的时间戳来生成ID(相当于允许时间静止)
4    currentTimestamp = lastTimestamp;
5    
6    // 但需要处理序列号溢出问题
7    // 如果同一毫秒内序列号用完,则自旋到下一毫秒(逻辑时间前进)
8}
9

这种做法的好处是对调用方完全透明,没有等待,性能好。但缺点是生成的ID中的"时间戳"可能略慢于实际物理时间,不过在大多数业务场景下是可接受的。

方案四:外部存储兜底(依赖Redis/ZooKeeper)

如果你们的系统对ID的唯一性要求极高,且服务器时钟经常大幅跳动,可以考虑引入外部组件来辅助。

核心逻辑

  • 发生时钟回拨时,不再依赖本地时间戳。
  • 向Redis请求一个自增的序号,或者查询ZooKeeper的zxid来作为时间戳的补充。
  • 这种做法会把雪花ID变成一个准分布式ID,强依赖于第三方组件的可用性,复杂度较高。

【分布式组件雪花ID】》 是转载文章,点击查看原文


相关推荐


Linux camera驱动开发(真正需要做的linux驱动开发)
嵌入式-老费2026/2/18

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】         很多的soc厂家,在发布sdk的时候,就提供了很多的芯片驱动。这里面有推荐的ddr、norflash、nandflash、emmc、sdio wifi、eth phy、触摸芯片等等。如果不是特殊的需求,基本上使用厂家推荐的芯片、模块,就可以做功能开发了。但是还有一些场景,是需要自己去主动适配驱动的,尤其是增加功能和降低成本的时候。 1、国产芯片适配      


OpenClaw架构揭秘:178k stars的个人AI助手如何用Gateway模式统一控制12+通讯频道
iDao技术魔方2026/2/9

一句话简介:178k stars 的开源项目 OpenClaw,用一套 Gateway 架构同时接入了 WhatsApp、Telegram、Slack、Discord 等 12+ 通讯频道,还实现了 Canvas 可视化、全时语音、浏览器控制等高级功能。这篇文章将深度拆解它的架构设计,告诉你一个「个人 AI 助手」应该如何构建。 📋 目录 背景:为什么需要个人AI助手? 项目概览:178k stars的OpenClaw 核心架构:Gateway WebSocket控制平面 多频道接入:1


墨梅博客 1.3.0 发布与服务器数据备份教训 | 2026 年第 5 周草梅周报
草梅友仁2026/2/1

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。 前言 欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。 开源动态 本周依旧在开发 墨梅 (Momei) 中。 您可以前往 Demo 站试用:demo.momei.app/ 您可以通过邮箱 admin@example.com,密码momei123456登录演示用管理


RPC分布式通信(3)--RPC基础框架接口
陌路202026/1/22

一、MprpcApplication 核心职责 MprpcApplication是 RPC 框架的 “管家”,核心作用: 单例模式:全局唯一实例,避免重复初始化; 配置加载:解析 RPC 框架的配置文件(如服务器 IP、端口、日志路径、注册中心地址等); 框架初始化:启动时初始化日志、网络、注册中心等核心组件; 全局参数访问:提供接口获取配置参数(如获取服务器端口、注册中心地址); 框架销毁:程序退出时释放资源。 二、MprpcApplication 核心接


【计算机网络 | 第三篇】MAC地址与IP地址
YYYing.2026/1/14

目录 MAC地址 一、MAC地址的格式特征 二、MAC地址的获取 三、什么是ARP? 四、ARP缓存 五、RARP IP地址 一、为什么要有IP地址? 二、既然IP地址存在,那它的意义是什么? 三、那又如何表示呢? 1、IP地址的定义 2、IPv4地址的表示方法 2.1、IPv4地址的分类编址方法 2.2、IPv4地址的划分子网编址方法 2.2.1、为什么要划分子网? 2.2.2、怎么划分子网? 2.2.3、总结 2.3、IPv4地址的无分类编址方法 3、构


Rust 的 `PhantomData`:零成本把“语义信息”交给编译器
Pomelo_刘金2026/1/5

在写底层 Rust(尤其是 unsafe / 裸指针 / FFI)时,你会遇到一种常见矛盾: 运行时:你手里可能只有一个 *const T / *mut T / *mut c_void(比如外部库返回的句柄),结构体里并没有真正存放某个引用或某个类型的值。 编译期:你又希望编译器知道“我这个类型和某个生命周期/类型绑定”,从而帮你做借用检查、推导 Send/Sync、避免错误混用等。 std::marker::PhantomData<T> 就是为了解决这个问题而存在的工具。官方文档的核心定义


前端开发者使用 AI 的能力层级——从表面使用到工程化能力的真正分水岭
月亮有石头2025/12/28

很多前端开发者已经在“使用 AI”: 会问问题、会让 AI 写代码、甚至在 IDE 里和 AI 对话。 但如果这些使用方式 无法稳定地产出可运行、可验证、可回归的工程结果, 那么严格来说——其实还没有真正入门。 这篇文章想系统回答一个问题: 前端开发者“使用 AI”的能力,是有明确层级和分水岭的。 不是工具多不多,也不是模型新不新, 而是:你用 AI 的方式,决定了它在你工程体系里的角色。 把 AI 放进工程链路,用工程约束对抗幻觉,用验证与反馈逼近真实。 AI 工程化的本质,并不是让模型


Node.js 编程实战:文件读写操作
程序员爱钓鱼2025/12/19

在后端开发中,文件读写是非常常见的需求,例如日志记录、配置文件管理、上传文件处理以及数据导入导出等。Node.js 提供了内置的 fs(File System)模块,使得我们可以高效地与文件系统进行交互。理解并掌握 Node.js 的文件读写方式,是每一个 Node.js 开发者的必备基础。 一、fs 模块简介 fs 模块是 Node.js 的核心模块之一,无需额外安装即可直接使用。它提供了同步和异步两套 API,用于完成文件的创建、读取、写入、删除以及目录操作等功能。 在实际开发中,Nod


大模型 MoE,你明白了么?
吴佳浩2025/12/11

大模型 MoE,你明白了么? 最近被T4卡搞得有点抽风就多些一点关于大模型的讲解的。由浅至深的讲个透,愿天下用老旧显卡的人儿都可以远离傻*问题。 作者:吴佳浩 最后更新:2025-12-11 适用人群:大模型上下游相关从业者 ——以 Qwen2/Qwen3 为例,从入门到回家 1. 什么是 MoE(Mixture of Experts) 核心概念 MoE = 混合专家模型,它让模型由多个"专家网络"组成,每次推理只激活少量专家,从而实现: ✅ 保留大模型能力 - 总参数量大,能力强 ✅


AI 计算模式(上)
兔兔爱学习兔兔爱学习2025/12/1

经典模型结构设计与演进 神经网络的基本概念 神经网络是 AI 算法基础的计算模型,灵感来源于人类大脑的神经系统结构。它由大量的人工神经元组成,分布在多个层次上,每个神经元都与下一层的所有神经元连接,并具有可调节的连接权重。神经网络通过学习从输入数据中提取特征,并通过层层传递信号进行信息处理,最终产生输出。这种网络结构使得神经网络在模式识别、分类、回归等任务上表现出色,尤其在大数据环境下,其表现优势更为显著。 对一个神经网络来说,主要包含如下几个知识点,这些是构成一个神经网络模型的基础组件。

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客