news 2026/3/10 20:23:35

C#.net 分布式ID之雪花ID,时钟回拨是什么?怎么解决?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#.net 分布式ID之雪花ID,时钟回拨是什么?怎么解决?

前言:雪花ID是一种分布式ID生成算法,具有趋势递增、高性能、灵活分配bit位等优点,但强依赖机器时钟,时钟回拨会导致ID重复或服务不可用。时钟回拨指系统时间倒走,可能由人为修改、NTP同步或硬件时钟漂移引起。基础解决方案是检测到回拨后抛出异常,但生产环境需要更优方案:1)缓存回拨时段ID,在允许范围内复用序列号;2)集群环境使用分布式缓存记录全局时间戳;3)使用逻辑时间戳彻底规避物理时钟依赖。建议优先采用缓存方案,设置合理的最大回拨时间(5-10秒),并监控告警回拨事件。

分布式ID 之雪花ID 时钟回拨是什么?怎么解决?

雪花ID 优缺点

优点:

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

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

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

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

C# .net雪花ID源码

using 520mus.top.SnowflakeId.Models; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; namespace 520mus.top.SnowflakeId.Service { public class SnowflakeIdService : ISnowflakeIdService { private const long twepoch = 687888001000L; // 起始时间戳(毫秒) private static readonly long workerIdBits = 5L; // 节点ID所占的位数 private static readonly long datacenterIdBits = 5L; // 数据中心ID所占的位数 private static readonly long maxWorkerId = -1L ^ (-1L << (int)workerIdBits); // 节点ID最大值 private static readonly long maxDatacenterId = -1L ^ (-1L << (int)datacenterIdBits); // 数据中心ID最大值 private static readonly long sequenceBits = 12L; // 序列号占用的位数 private static readonly long workerIdShift = sequenceBits; // 节点ID左移位数 private static readonly long datacenterIdShift = sequenceBits + workerIdBits; // 数据中心ID左移位数 private static readonly long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 时间戳左移位数 private static readonly long sequenceMask = -1L ^ (-1L << (int)sequenceBits); // 用于掩码序列号 private long lastTimestamp = -1L; // 上次生成ID的时间戳 private long workerId; // 节点ID private long datacenterId; // 数据中心ID private long sequence = 0L; // 序列号 private SnowflakeIdSetting _config; public SnowflakeIdService() { _config = new SnowflakeIdSetting(); this.workerId = _config.MachineId; this.datacenterId = _config.DataCenterId; if (workerId > maxWorkerId || workerId < 0) { throw new ArgumentException($"Worker ID 必须在 0 到 {maxWorkerId} 之间"); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new ArgumentException($"Datacenter ID 必须在 0 到 {maxDatacenterId} 之间"); } } public SnowflakeIdService(IOptionsMonitor<SnowflakeIdSetting> config) { _config = config.CurrentValue; this.workerId = _config.MachineId; this.datacenterId = _config.DataCenterId; if (_config == default) { _config = new SnowflakeIdSetting(); } if (workerId > maxWorkerId || workerId < 0) { throw new ArgumentException($"Worker ID 必须在 0 到 {maxWorkerId} 之间"); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new ArgumentException($"Datacenter ID 必须在 0 到 {maxDatacenterId} 之间"); } } public long GetNextId() { lock (this) // 加锁保证多线程安全 { long timestamp = TimeGen(); if (timestamp < lastTimestamp) { throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds"); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = TilNextMillis(lastTimestamp); } } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - twepoch) << (int)timestampLeftShift) | (datacenterId << (int)datacenterIdShift) | (workerId << (int)workerIdShift) | sequence; } } private long TilNextMillis(long lastTimestamp) { long timestamp = TimeGen(); while (timestamp <= lastTimestamp) { timestamp = TimeGen(); } return timestamp; } private long TimeGen() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // 获取当前时间戳(毫秒) } }

model

namespace 520mus.top.SnowflakeId.Models { public class SnowflakeIdSetting { /// <summary> /// 机器标识Id /// </summary> public long MachineId { get; set; } = 21; /// <summary> /// 数据中心标识Id /// </summary> public long DataCenterId { get; set; } = 11; } }

一、先理解:什么是机器时钟回拨?

在雪花算法中,时钟回拨(Clock moved backwards)指的是:当前生成 ID 时获取到的系统时间戳(毫秒级),小于上一次生成 ID 时记录的时间戳(lastTimestamp),简单说就是「系统时间倒着走了」。

为什么会出现时钟回拨?

  1. 人为修改系统时间:运维人员手动将服务器时间调早(比如从 10:00 改到 09:59)。
  2. NTP 时间同步:服务器开启了网络时间协议(NTP),当本地时间快于标准网络时间时,NTP 服务会将本地时间「校准回退」(而非平滑推进),这是生产环境中最常见的原因。
  3. 系统时钟漂移:服务器硬件时钟(RTC)出现偏差,导致系统时间异常回退。

对雪花算法的影响?

雪花算法的核心依赖「时间戳递增」来保证 ID 的全局唯一性和有序性,如果出现时钟回拨,会直接导致:

  • 生成重复的 ID(因为时间戳回退,加上节点 ID、序列号可能复用)。
  • 抛出你代码中的异常,导致 ID 生成服务不可用。

二、当前代码的解决方案(基础方案)

上面的这段雪花算法代码,已经内置了一种简单的时钟回拨应对方案,我们来分析它的处理逻辑:

1. 核心检测逻辑(GetNextId方法中)

if (timestamp < lastTimestamp) { throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds"); }

这一步是先检测:如果当前时间戳小于上一次的时间戳,直接抛出异常,拒绝生成 ID,避免生成重复 ID。

2. 同一时间戳内的序列号耗尽处理(间接规避轻微时钟抖动)

if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 序列号耗尽时,等待到下一个毫秒 if (sequence == 0) { timestamp = TilNextMillis(lastTimestamp); } }

对应的TilNextMillis方法是「自旋等待」:

private long TilNextMillis(long lastTimestamp) { long timestamp = TimeGen(); // 循环等待,直到获取到大于上一次时间戳的新时间戳 while (timestamp <= lastTimestamp) { timestamp = TimeGen(); } return timestamp; }

这个方法的作用是:当同一毫秒内的序列号(12 位,最多 4096 个)耗尽时,等待到下一个毫秒再生成 ID,本质上是应对「同一毫秒内请求过多」,但也能处理「极轻微的时钟抖动(回拨时间小于 1 毫秒)」。

3. 当前方案的局限性

这个基础方案只能处理「极轻微的时钟回拨(<1 毫秒)」,对于「明显的时钟回拨(>1 毫秒,比如 NTP 校准回退了几秒)」,直接抛出异常,会导致服务中断,这在生产环境中是不可接受的。

三、生产环境更优的时钟回拨解决方案

针对明显的时钟回拨,有以下 3 种主流解决方案,从易到难排序:

方案 1:缓存回拨时段的 ID(推荐,实现简单)

核心思路:当检测到时钟回拨时,不直接抛出异常,而是在允许的回拨时间范围内(比如 5 秒),复用「回拨时段的序列号」,保证 ID 不重复且有序

实现步骤:

  1. 新增配置项:允许的最大回拨时间(如MaxClockBackwardMs = 5000,5 秒)。
  2. 新增缓存容器:用于存储「回拨时段已生成的序列号」,避免重复(可使用Dictionary<long, long>,key 为时间戳,value 为该时间戳已使用的最大序列号)。
  3. 改造检测逻辑:
    • 如果回拨时间超过最大允许值,直接抛出异常(避免缓存过多,占用内存)。
    • 如果回拨时间在允许范围内,从缓存中获取该时间戳的最新序列号,递增后生成 ID,并更新缓存。

改造后的核心代码片段(关键部分):

// 新增:允许的最大回拨时间(可配置) private readonly long _maxClockBackwardMs = 5000; // 新增:缓存回拨时段的时间戳与对应最大序列号 private readonly Dictionary<long, long> _backwardTimestampSequenceCache = new Dictionary<long, long>(); public long GetNextId() { lock (this) { long timestamp = TimeGen(); long currentSequence = 0; // 处理时钟回拨 if (timestamp < lastTimestamp) { long backwardMs = lastTimestamp - timestamp; // 超过最大允许回拨时间,抛出异常 if (backwardMs > _maxClockBackwardMs) { throw new Exception($"时钟回拨超过允许最大值({_maxClockBackwardMs}ms),拒绝生成ID,回拨时长:{backwardMs}ms"); } // 回拨时间在允许范围内,从缓存中获取序列号 if (_backwardTimestampSequenceCache.ContainsKey(timestamp)) { currentSequence = _backwardTimestampSequenceCache[timestamp] + 1; // 检查序列号是否耗尽 if (currentSequence > sequenceMask) { // 该时间戳序列号耗尽,等待到下一个毫秒(复用原有方法) timestamp = TilNextMillis(lastTimestamp); currentSequence = 0; } else { _backwardTimestampSequenceCache[timestamp] = currentSequence; } } else { // 该回拨时间戳首次使用,初始化序列号为0 _backwardTimestampSequenceCache.Add(timestamp, 0); currentSequence = 0; } } else if (timestamp == lastTimestamp) { // 同一时间戳,序列号递增(原有逻辑) currentSequence = (sequence + 1) & sequenceMask; if (currentSequence == 0) { timestamp = TilNextMillis(lastTimestamp); currentSequence = 0; } } else { // 时间戳递增,清空回拨缓存(释放内存) _backwardTimestampSequenceCache.Clear(); currentSequence = 0; } // 更新最后时间戳和当前序列号 lastTimestamp = timestamp; sequence = currentSequence; // 生成ID(原有逻辑不变) return ((timestamp - twepoch) << (int)timestampLeftShift) | (datacenterId << (int)datacenterIdShift) | (workerId << (int)workerIdShift) | currentSequence; } }

方案 2:使用分布式缓存记录全局最新时间戳(集群环境)

如果你的服务是集群部署(多个节点生成 ID),方案 1 的本地缓存无法保证全局唯一性,此时可以使用分布式缓存(Redis/Memcached)记录「全局最新的时间戳和对应序列号」。

核心思路:

  1. 每个节点生成 ID 时,先从 Redis 中获取全局最新时间戳(global_last_timestamp)和全局最新序列号(global_last_sequence)。
  2. 对比本地时间戳和全局最新时间戳,处理时钟回拨(逻辑同方案 1,只是缓存从本地改为 Redis)。
  3. 生成 ID 后,更新 Redis 中的全局时间戳和序列号(使用 Redis 的原子操作,如SETNXINCR,避免并发问题)。

优势:适合集群环境,保证全局 ID 唯一性;缺点:依赖分布式缓存,增加系统复杂度和性能开销。

方案 3:使用逻辑时间戳(彻底规避物理时钟依赖)

核心思路:不依赖系统物理时钟,而是使用「自增的逻辑时间戳」,彻底摆脱时钟回拨的影响。

实现步骤:

  1. 启动服务时,记录当前物理时间戳作为初始逻辑时间戳。
  2. 每次生成 ID 时,逻辑时间戳自增(而非获取系统物理时间戳),可以按毫秒粒度自增(如每毫秒自增 1,或每生成一个 ID 自增 1,再映射为毫秒级)。
  3. 定期同步物理时间戳,校准逻辑时间戳(避免逻辑时间戳与物理时间戳偏差过大)。

优势:彻底解决时钟回拨问题;缺点:实现复杂,集群环境下需要保证逻辑时间戳的全局唯一性(依赖分布式锁或中心节点分配),适合对 ID 生成稳定性要求极高的场景。

四、补充建议

  1. 生产环境中,优先选择方案 1(缓存回拨时段的 ID),兼顾实现简单和稳定性,建议将最大允许回拨时间设置为 5-10 秒(覆盖大部分 NTP 校准场景)。
  2. 开启 NTP 服务时,配置为「平滑同步」(如 Linux 的ntpd服务,默认平滑同步,不会直接回退时间;避免使用ntpdate强制回退时间)。
  3. 给雪花算法服务添加监控告警,当出现时钟回拨(即使在允许范围内)时,及时通知运维人员排查,避免潜在问题。

总结

  1. 时钟回拨是指生成 ID 的当前物理时间戳小于上一次的时间戳,会导致雪花算法生成重复 ID 或服务中断,常见原因是 NTP 时间校准。
  2. 基础解决方案是自旋等待下一个毫秒(应对 < 1 毫秒回拨),但生产环境需用「缓存回拨时段序列号」应对明显回拨。
  3. 集群环境可选择分布式缓存记录全局时间戳,极高稳定性要求场景可使用逻辑时间戳彻底规避物理时钟依赖。

其他

【电商 】订单减少库存业务流程,分布式ID策略选型,C# .net雪花ID代码,下单功能实现 ,异步延时队列

https://blog.csdn.net/cao919/article/details/126413455

Net 模拟退火,遗传算法,禁忌搜索,神经网络 ,并将 APS 排程算法集成到 ABP vNext 中

https://blog.csdn.net/cao919/article/details/155564023

SAAS多租户套餐权限模块功能按钮 设置 关键代码实现 JAVA C#

https://blog.csdn.net/cao919/article/details/143254585

在C# .net中RabbitMQ的核心类型和属性,除了交换机,队列关键的类型 / 属性,影响其行为

https://blog.csdn.net/cao919/article/details/157254797

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/10 8:07:36

基于python的学习资源分享系统vue3

目录 Python学习资源分享系统&#xff08;Vue3&#xff09;摘要 开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; Python学习资源分享系统&#xff08;Vue3&#xff09;摘要 系统概述 该系统…

作者头像 李华
网站建设 2026/3/10 5:30:03

45年数论猜想被GPT-5.2 Pro独立完成证明,陶哲轩:没犯任何错误

45年数论猜想被GPT-5.2 Pro独立完成证明&#xff0c;陶哲轩&#xff1a;没犯任何错误 关注前沿科技 量子位 2026年1月19日 15:00 北京 梦晨 发自 凹非寺 量子位 | 公众号 QbitAI AI证明数学猜想&#xff0c;这次来真的了。 OpenAI最新模型GPT-5.2 Pro刚刚独立证明了一道埃尔…

作者头像 李华
网站建设 2026/3/8 16:42:02

演讲回顾|Apache Pulsar x AI Agent:智能系统消息基础架构

本文整理自 翟佳 在2025 GOTC 全球开源技术峰会上的演讲&#xff0c;一起来看 Pulsar 如何赋能多 Agent 协同&#xff5e; Pulsar 的云原生架构 Pulsar 的架构演进深植于云原生技术的发展脉络。其设计旨在满足现代应用对运营效率的高要求&#xff0c;技术根源可追溯至 20 世纪 …

作者头像 李华
网站建设 2026/3/7 9:32:07

2026年【具身智能】微信群成立!

点击下方卡片&#xff0c;关注“CVer”公众号AI/CV重磅干货&#xff0c;第一时间送达具身智能&#xff1a;人工智能的下一个浪潮&#xff01;今年首次被写入《政府工作报告》中&#xff0c;已经成为国家未来重点培育产业。市场方面&#xff0c;具身智能近一年融资更是爆火&…

作者头像 李华
网站建设 2026/3/7 0:10:16

sprintf在嵌入式开发中的5个典型应用案例

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个嵌入式系统模拟器&#xff0c;演示sprintf在以下场景的应用&#xff1a;1)将ADC采样值格式化为带单位的字符串(如"电压:3.3V")&#xff1b;2)组装Modbus协议数据…

作者头像 李华
网站建设 2026/3/3 14:19:34

如何用3步解决C盘爆满难题:Windows Cleaner实战指南

如何用3步解决C盘爆满难题&#xff1a;Windows Cleaner实战指南 【免费下载链接】WindowsCleaner Windows Cleaner——专治C盘爆红及各种不服&#xff01; 项目地址: https://gitcode.com/gh_mirrors/wi/WindowsCleaner 诊断磁盘健康状态 识别C盘爆红的5大典型症状 当…

作者头像 李华