雪花算法重复id问题

原理解析

雪花算法实现简单、适配性强,无论是电商订单、日志追踪还是分布式存储,都能满足 “唯一、有序、高效、可扩展” 的核心需求,因此成为分布式ID主流选择。雪花算法生成的ID是一个64位的整数,由多段不同意义的数字拼接而成,这种分段设计让每个ID既带着时间印记,又能规避多机器冲突,就像身份证通过地址码、出生日期码、顺序码等分段信息实现全国唯一标识,既有序又精准。

符号位 + 时间戳 + 数据中心ID + 机器ID + 序列号

  1. 符号位(1位):始终为0(表示正数)。这保证了生成的 ID 是正整数。
  2. 时间戳(41位):雪花算法的核心部分, 记录生成ID时的毫秒级时间戳(当前时间减去起始时间的差值),该部分保证了ID的大体有序性。41位能表示的时间范围约为 2^41 毫秒 ≈ 69年。使用一个最近的起始时间(如 2025-01-01
    00:00:00),可以大幅减少时间戳占用的位数。
  3. 数据中心ID(5位):用于标识生成ID的逻辑数据中心,允许最多 2^5 = 32个数据中心。
  4. 工作节点ID(5位):用于标识数据中心内的具体工作节点(机器、服务进程、Pod 等),允许每个数据中心最多 2^5 =
    32个工作节点。在实际开发中,数据中心(高位)+
    工作节点(低位)经常被视为一个整体10位的机器ID,用于标识集群中的唯一节点(机器/服务实例),最多允许 2^10 =
    1024个唯一节点。
  5. 序列号(12位):用来解决同一节点在同一毫秒内生成多个 ID
    时的冲突问题。每个节点在每毫秒内都可以独立地从0开始递增生成序列号,当序列号用完(达到
    4095)后,会强制等待到下一毫秒再继续生成。对于12位序列号,单节点每毫秒最多生成4096个ID,要达到这个并发量很极端(单节点超过400万QPS),现实中很难溢出。

以下为java实现的雪花算法代码示例(未考虑时钟回拨),起始时间决定了算法能生成ID的有效时长,通常将起始时间设为项目上线日期。

public class SnowflakeIdGenerator {// 起始时间戳,这里以2025-07-01 00:00:00为基准private final long startTimeStamp = 1751299200000L;// 机器ID所占位数private final long workerIdBits = 5L;// 数据中心ID所占位数private final long dataCenterIdBits = 5L;// 序列号所占位数private final long sequenceBits = 12L;// 机器ID最大值 31private final long maxWorkerId = -1L ^ (-1L << workerIdBits);// 数据中心ID最大值 31private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);// 机器ID向左移位数private final long workerIdShift = sequenceBits;// 数据中心ID向左移位数private final long dataCenterIdShift = sequenceBits + workerIdBits;// 时间戳向左移位数private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;// 序列号掩码 4095private final long sequenceMask = -1L ^ (-1L << sequenceBits);// 工作机器IDprivate final long workerId;// 数据中心IDprivate final long dataCenterId;// 序列号private long sequence = 0L;// 上次生成ID的时间戳private long lastTimestamp = -1L;// 构造函数public SnowflakeIdGenerator(long workerId, long dataCenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException("Worker ID 不能大于 " + maxWorkerId + " 或小于 0");}if (dataCenterId > maxDataCenterId || dataCenterId < 0) {throw new IllegalArgumentException("数据中心 ID 不能大于 " + maxDataCenterId + " 或小于 0");}this.workerId = workerId;this.dataCenterId = dataCenterId;}// 生成下一个IDpublic synchronized long nextId() {long currentTimestamp = System.currentTimeMillis();if (currentTimestamp == lastTimestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 当前毫秒内序列号已用完,等待下一毫秒currentTimestamp = waitNextMillis(lastTimestamp);}} else {// 时间戳改变,重置序列号sequence = 0L;}lastTimestamp = currentTimestamp;// 按规则组合生成IDreturn ((currentTimestamp - startTimeStamp) << timestampShift) |(dataCenterId << dataCenterIdShift) |(workerId << workerIdShift) |sequence;}// 等待下一毫秒private long waitNextMillis(long lastTimestamp) {long timestamp = System.currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = System.currentTimeMillis();}return timestamp;}// 测试示例public static void main(String[] args) {SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);for (int i = 0; i < 10; i++) {System.out.println(idGenerator.nextId());}}
}    

4.2、为什么会出现重复ID?

雪花算法虽代码量少、实现简单,却并非万无一失。不少研发人员常常直接从网上拷贝现成的工具类,或是用大模型生成代码后直接用于生产环境 —— 直到某天突然收到用户反馈:自己账号的数据出现了错乱,明明只买了一件衣服,订单却显示多个其他辣眼的商品,还附带陌生的收货地址。手忙脚乱一顿排查后,竟发现数据库中出现了少量订单SN重复的异常数据,不由得心生疑惑:雪花算法不是每毫秒能生成4096个不重复编号吗?订单服务部署了十几个节点,但业务量真有这么大吗?到底为什么会出现重复呢?我们一起来一探究竟。

4.2.1、机器ID重复

  • 为什么重复:
    多个运行的节点使用相同的数据中心ID(datacenter-id)和工作节点ID(worker-id)。即在同一毫秒内,如果多个节点的机器ID相同、系统时间戳相同,序列号就可能从相同起点开始分配并重叠,导致生成完全相同的ID三元组
    (时间戳, 机器ID, 序列号)。

  • 典型现象: 多数研发人员会将数据中心ID和工作节点ID硬编码在代码中,或在配置文件里设置了相同的
    datacenter-id与worker-id,这直接导致无论部署多少个节点,机器ID都完全一致。

  • 如何解决:

核心原则必须确保整个分布式集群中,任何两个同时工作的节点,它们的 (数据中心ID, 工作节点ID) 二元组(或者将二者视为10位合并的“机器ID”)必须是唯一的!

(1)手动配置文件:在启动服务前,为每个节点的配置文件(如 application.properties, application.yml, configmap 等)显式配置一个唯一的 datacenter-id 和 worker-id。该方案简单直观,但繁琐,易出错(配置冲突),适合小型、静态集群,不适用于节点动态伸缩的集群。

(2)系统环境变量:在部署节点(物理机、虚拟机、容器)时,通过启动脚本、容器编排系统(如K8s Deployment/StatefulSet 的env)为每个实例设置唯一的 SNOWFLAKE_DATACENTER_ID和SNOWFLAKE_WORKER_ID环境变量,服务启动时读取这些环境变量。

(3)利用基础设施的唯一性:

  • Kubernetes StatefulSet会为每个 Pod
    分配一个固定且有序的唯一索引(从0开始)。比如名为snowflake-app的StatefulSet有3个Pod:snowflake-app-0,
    snowflake-app-1, snowflake-app-2。应用程序可以读取 spec.podName(通常是
    HOSTNAME环境变量),解析末尾的数字索引,将这个索引直接用作工作节点ID。若业务需要扩容至超过worker-id最大阈值(如32个以上Pod),直接使用索引会导致worker-id重复,需结合数据中心ID(datacenter-id)拆分(如用 StatefulSet 名称哈希作为datacenter-id)。

  • 公有云(如阿里云、华为云、腾讯云ECS)会为每个虚拟机实例分配一个唯一ID,Pod(如Deployment)运行时也有自己的ID。应用程序可以在启动时通过查询实例/容器的元数据服务获取这个唯一ID,然后对这个较长的ID进行哈希并取模,映射到可用的datacenter-id和worker-id范围内(如总ID%1024,得到 0-1023的一个值)。该方案需要依赖特定平台的 API/服务。哈希取模存在极小冲突风险,需要设计好映射逻辑。

  • 利用IP地址 (网络标识):应用程序直接获取其运行环境(Pod、容器、虚拟机、物理机)的IP地址,对整个IP地址字符串或二进制表示计算哈希值取模,然后取模,映射为datacenter-id和worker-id。在Kubernetes 中,在Kubernetes中,Pod通常可以通过status.podIP获得,Deployment Pod重建通常会获得新IP;虚拟机/物理机IP也可能因维护、迁移或网络配置变更而改变。该方案同样存在极小概率ID冲突,且需容忍获取IP的性能开销和失败风险。

// 获取机器ID
private static long getNodeId() {try {InetAddress address = findFirstNonLoopbackAddress();String ip = address.getHostAddress();int hash = ip.hashCode();// 确保非负数并取模最大节点IDlong nodeId = (hash & 0x7FFFFFFF) % (MAX_NODE_ID + 1);System.out.println("使用IP地址: " + ip + " 生成机器ID: " + nodeId);return nodeId;} catch (Exception e) {// 异常时随机生成节点IDlong nodeId = new Random().nextInt((int) (MAX_NODE_ID + 1));System.out.println("获取IP失败,随机生成机器ID: " + nodeId);return nodeId;}
}// 查找第一个非环回IPv4地址
private static InetAddress findFirstNonLoopbackAddress() throws SocketException {Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();while (interfaces.hasMoreElements()) {NetworkInterface iface = interfaces.nextElement();if (iface.isLoopback() || iface.isVirtual() || !iface.isUp()) {continue;}Enumeration<InetAddress> addresses = iface.getInetAddresses();while (addresses.hasMoreElements()) {InetAddress addr = addresses.nextElement();if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {return addr;}}}throw new RuntimeException("未找到非环回IPv4地址");
}

(4)外部协调服务:使用分布式协调服务(如ZooKeeper, etcd, Redis, 数据库)来注册节点并分配唯一的机器 ID。如Leaf-Snowflake改进了雪花算法,机器ID由Zookeeper协调分配、百度UID Generator启动时向DB注册节点分配唯一worker_id。

  • 流程示例:
  1. 节点启动时,连接到协调服务。

  2. 如果节点宕机或与协调服务断开连接(session超时),协调服务会自动删除其对应的临时节点,该机器ID被释放,可以被新节点申请使用。

  3. 节点将这个唯一的序号作为它的机器ID(或从中计算 datacenter-id 和 worker-id,如序号 % 1024)。序号在服务运行期间保持不变。

  4. 节点读取自己创建的节点的序号(如 0000000005)。

  5. 协调服务保证创建的有序节点的名称(包含一个单调递增的序号)是唯一的。

  6. 节点尝试在一个预设的路径下(如 /snowflake/workers)创建一个临时有序节点。

  • 优点: 无需预配置,自动处理节点加入/离开,ID分配唯一且可靠,支持大规模集群。

  • 缺点: 增加了外部依赖和复杂度。

(5)设计机器ID位数的考虑:默认10位能支持 1024 个节点,对大多数公司规模通常够用。可依据业务规模灵活调整:

  • 并发量高但集群规模不大(节点少): 可以减少datacenter-id和worker-id 总位数(比如降到8位甚至更少),把节省出来的位数加到sequence序列号上。这样每个节点每毫秒可以生成更多的ID。
  • 集群规模巨大(超过1024节点): 需要增加datacenter-id和worker-id总位数(比如设为12位)。这时需要牺牲timestamp或sequence 的位数(如时间戳减到40,序列号减到11位)。牺牲时间戳位数会缩短系统的可用年限;牺牲序列号会降低单节点/毫秒的最大并发量。

4.2.2、时钟回拨

  • 为什么重复:系统时间因为NTP同步失败、闰秒调整、虚拟机/容器挂起恢复、人为设置错误等原因发生了向后跳跃,导致雪花算法生成ID时使用了之前已生成ID的时间戳部分,进而可能产生重复ID。

  • 如何解决:大部分雪花算法的优秀实现都包含了时钟回拨检测和处理机制,如抛出异常、短暂等待、使用备用逻辑。

(1)预防为主:禁止手动时间修改;NTP通过频率调整、分散度控制、时钟筛选、步进限制等机制防止时间回拨,如使用chrony进行平滑时间调整(stepping → slewing)、配置clock slew而非 jump避免突变。

(2)抛出异常:当检测到时钟回拨时,直接抛出异常,停止生成ID,等待人工干预或时间恢复正常。该方案简单安全,但影响业务连续性。

//处理时钟回拨
if (currentTimestamp < lastTimestamp) {throw new ClockBackwardException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - currentTimestamp) + " milliseconds");
}

(3)等待时钟恢复(适合毫秒级轻度回拨):若发现回拨,不立即报错,而是阻塞等待 ,直到系统时间 ≥ lastTimestamp。该方案短暂阻塞,可能影响性能。

// 处理时钟回拨
if (currentTimestamp < lastTimestamp) {long offset = lastTimestamp - currentTimestamp;// 回拨时间小于1秒,阻塞等待if (offset <= MAX_BACKWARD_TIME) {currentTimestamp = waitForClockRecovery(lastTimestamp);} else {// 回拨时间超过1秒,抛出异常throw new RuntimeException("Clock moved backwards too much: " + offset + "ms");}
}private long waitForClockRecovery(long lastTimestamp) {long timestamp = System.currentTimeMillis();while (timestamp < lastTimestamp) {//短暂休眠避免CPU空转try {Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("Interrupted while waiting for clock recovery", e);}timestamp = System.currentTimeMillis();}System.out.println("Clock recovered after " + (timestamp - lastTimestamp) + "ms");return timestamp;
}

(4)回拨补偿:通过累积所有历史回拨时间,使生成器内部时间永远领先于系统时间,可避免使用Thread.sleep()造成的性能瓶颈。

// 可容忍的最大时钟回拨(毫秒)
private static final long MAX_BACKWARD_MS = 1000;// 发生时钟回拨
if (currentTimestamp < lastTimestamp) {long backwardMs = lastTimestamp - currentTimestamp;// 超过容忍阈值则抛出异常if (backwardMs > MAX_BACKWARD_MS) {throw new IllegalStateException("Clock moved backwards by " + backwardMs + " ms, exceeding maximum allowed value");}// 记录回拨时间用于补偿clockOffset += backwardMs;// 补偿当前时间戳currentTimestamp = System.currentTimeMillis()+clockOffset;
}

(5)扩展位机制(秒级以上严重回拨):修改雪花算法结构,预留几位用于表示“是否处于回拨状态”或“回拨次数”。当发生回拨时,增加“回拨版本号”,即使时间戳相同,版本不同也能区分ID。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91907.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91907.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MQTT 入门教程:三步从 Docker 部署到 Java 客户端实现

在物联网&#xff08;IoT&#xff09;与边缘计算快速发展的今天&#xff0c;设备间的高效通信成为核心需求。MQTT 作为一种轻量级的发布 / 订阅模式协议&#xff0c;凭借其低带宽占用、强稳定性和灵活的消息路由能力&#xff0c;已成为物联网通信的事实标准。无论是智能家居的设…

公网服务器上Nginx或者Openresty如何屏蔽IP直接扫描

0x01 背景云服务器很多时候为了通信需要设置公网访问&#xff0c;但是网络当中存在很多的扫描器&#xff0c;无时无刻在扫描&#xff0c;当80,443端口暴露时&#xff0c;成了这些扫描IP的攻击对象&#xff0c;无时无刻收到威胁。0x02 扫描攻击方式1.直接通过公网IP地址进行一些…

C语言(长期更新)第8讲 函数递归

C语言&#xff08;长期更新&#xff09; 第8讲:函数递归 跟着潼心走&#xff0c;轻松拿捏C语言&#xff0c;困惑通通走&#xff0c;一去不回头~欢迎开始今天的学习内容&#xff0c;你的支持就是博主最大的动力。 目录 C语言&#xff08;长期更新&#xff09; 第8讲 函数递归…

[硬件电路-129]:模拟电路 - 继电器的工作原理、关键指标、常用芯片与管脚定义

一、工作原理继电器是一种基于电磁感应原理的自动开关装置&#xff0c;通过控制小电流电路实现大电流电路的通断。其核心结构包括&#xff1a;电磁铁&#xff08;线圈铁芯&#xff09;&#xff1a;通电时产生磁场&#xff0c;吸引衔铁动作。触点系统&#xff1a;包含常开触点&a…

Haproxy调度算法 - 静态算法介绍与使用

文章目录一、概述二、socat工具三、static-rr四、firstHAProxy通过固定参数 balance 指明对后端服务器的调度算法&#xff0c;该参数可以配置在listen或backend选项中。HAProxy的调度算法分为静态和动态调度算法&#xff0c;但是有些算法可以根据参数在静态和动态算法中相互转换…

模拟激光相机工作站版本6.0 5.2.32 6.0.44 6.031 5.2.20

模拟激光相机工作站版本6.0 5.2.32 6.0.44 6.031 5.2.20

AWS Blockchain Templates:快速部署企业级区块链网络的终极解决方案

无需精通底层架构&#xff0c;一键搭建Hyperledger Fabric或以太坊网络&#xff01;AWS Blockchain Templates 可帮助您快速基于不同的区块链框架在 AWS 上创建和部署区块链网络。区块链是一种分布式数据库技术&#xff0c;用于维护不断增长的交易记录和智能合约集合&#xff0…

Vue 服务端渲染 Nuxt 使用详解

Nuxt 是基于 Vue 的高层框架&#xff0c;专注于服务器端渲染应用开发。它封装了繁琐的配置和通用模式&#xff0c;提供了开箱即用的 SSR 功能&#xff0c;使开发者能够专注于编写业务逻辑。 1. Nuxt 的核心特性 SSR 支持&#xff1a;默认支持服务端渲染&#xff0c;提高应用性…

使用ACK Serverless容器化部署大语言模型FastChat

核心概念 阿里云ACK Serverless&#xff1a;是一种基于 Kubernetes 的无服务器容器服务。用户无需管理底层节点和服务器&#xff0c;即可快速部署容器化应用&#xff0c;并根据实际使用的 CPU 和内存资源按需付费&#xff0c;只专注于应用本身而非基础设施管理。 FastChat&…

最新Android Studio汉化教程--兼容插件包

[ ] 软件版本&#xff1a;Android Studio Meerkat Feature Drop | 2024.3.2 Build #AI-243.25659.59.2432.13423653, built on April 30, 2025 Runtime version: 21.0.613368085-b895.109 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Toolkit: sun.awt.windows.WT…

Unity_数据持久化_IXmlSerializable接口

Unity数据持久化 三、XML数据持久化 3.5 IXmlSerializable接口 3.5.1 IXmlSerializable接口基础概念 什么是IXmlSerializable接口&#xff1a; IXmlSerializable 是.NET框架提供的一个接口&#xff0c;允许类自定义XML序列化和反序列化的过程。当默认的XML序列化行为无法满足需…

如何快速解决PDF解密新方法?

有时从网络下载的PDF文档会带有加密限制&#xff0c;导致无法编辑、复制或打印。它的体积仅约10MB&#xff0c;无需安装&#xff0c;解压即用。遇到受限制的文件时&#xff0c;只需将其拖入界面&#xff0c;选择是否覆盖原文件&#xff0c;点击执行&#xff0c;瞬间完成解密。「…

译|数据驱动智慧供应链的构成要素与关联思考

数据质量&#xff0c;通过识别关键决策和瓶颈构建信息供应链。该模型适用于优化库存管理、自动化物流、预测需求、实现产品全生命周期追溯及应对突发风险。例如&#xff0c;通过AI机器人自动管理仓库&#xff0c;或利用数字孪生模拟和优化全球采购网络。 汇总来自三篇文章&…

OS21.【Linux】环境变量

目录 1.与环境变量有关的实验 A.对比命令和自制程序的运行 为什么.像ls、pwd这样的命令运行是不需要加路径? 执行自制程序而不加路径的方法,看看PATH环境变量 方法1:将自制程序移动到系统的搜索路径下 方法2:临时修改PATH环境变量 B.查看系统中所有环境变量 解释几个常…

加密流量论文复现:《Detecting DNS over HTTPS based data exfiltration》(上)

本文将以我个人的理解去阅读该篇流量加密论文&#xff0c;并在下一篇尽力对其中的实验部分进行复现。话不多说&#xff0c;先从论文开始着手。 内容介绍 传统的DNS(Domain Name System)协议是以明文传输的。DNS作为互联网的基础设施&#xff0c;最初设计时主要考虑的是功能和效…

Apache RocketMQ 中Message (消息)的核心概念

好的&#xff0c;我们来深入理解一下 Apache RocketMQ 中 Message (消息) 这个核心概念。这份文档详细阐述了消息的定义、在模型中的位置、内部属性、约束和使用建议。 你可以将 Message 看作是 RocketMQ 系统中数据传输和处理的最小原子单位。它承载了业务数据&#xff0c;并附…

C 语言问题

1. C语言中 union 与 struct 的区别类型structunion内存分配机制编译器为每个成员‌独立分配内存空间&#xff0c;总内存大小 所有成员大小之和&#xff08;考虑内存对齐&#xff09;所有成员‌共享同一段内存空间&#xff0c;总内存大小 ‌最大成员的大小‌数据存储特性1. 所…

[ LeetCode优选算法专题一双指针-----盛最多的水]

1.题目链接 LeetCode盛最多的水 2.题目描述 3.题目解析 问题本质分析 "盛最多水的容器" 问题可以抽象为&#xff1a;在坐标轴上有 n 条垂直线段&#xff0c;第 i 条线段的两个端点分别是 (i, 0) 和 (i, height [i])。找到两条线段&#xff0c;使得它们与 x 轴共同…

旧笔记本电脑如何安装飞牛OS

01引言随着电子产品的更新换代&#xff0c;我们有很多的电子产品已经满足不了现在的工作需求和日常娱乐了&#xff0c;比如&#xff1a;用了很久厚重笔记本电脑放在现在办公也是有点吃力了&#xff0c;我们现在换新了旧的还不想放在那里吃灰&#xff0c;怎么办呢&#xff1f;我…

某金服Java面试终极指南:25题完整解析与场景化方案

涵盖分布式锁、缓存、事务、高并发等金融系统核心考点&#xff0c;附解决方案与抗风险设计一、分布式锁深度解决方案 1. Redis分布式锁完整实现 // 原子加锁 防死锁 String uuid UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue().setIfAbsent(&qu…