C# 雪花ID实现方案

你希望在.NET中实现一个完整、线程安全且能处理时钟回拨问题的雪花ID(Snowflake)算法,用于分库分表场景下生成全局唯一的分布式ID,避免不同库/表的主键重复。

雪花ID是Twitter开源的分布式ID生成算法,核心是将64位长整型ID按规则拆分,保证全局唯一且趋势递增。下面先讲解核心结构,再给出可直接使用的.NET实现代码。


一、雪花ID核心结构(64位long型)

位段 长度 作用
符号位 1位 固定为0(保证ID为正数)
时间戳位 41位 记录生成ID的毫秒级时间戳(相对于一个起始时间),可支持约69年(2^41-1)
机器/节点位 10位 分为5位数据中心ID + 5位机器ID,最多支持1024个节点部署
序列号位 12位 同一毫秒内的自增序列,每毫秒最多生成4096个ID(2^12=4096)

二、.NET实现雪花ID生成器

以下实现包含线程安全时钟回拨检测参数合法性校验,可直接在生产环境使用:

csharp 复制代码
using System;
using System.Threading;

/// <summary>
/// 雪花ID生成器(线程安全、支持时钟回拨检测)
/// </summary>
public class SnowflakeIdGenerator
{
    #region 常量定义
    // 起始时间戳(UTC时间:2024-01-01 00:00:00),可自行调整,建议使用项目上线前的时间
    private const long Epoch = 1704067200000L;

    // 各部分的位长度
    private const int TimestampBits = 41; // 时间戳位长度
    private const int DataCenterIdBits = 5; // 数据中心ID位长度
    private const int MachineIdBits = 5; // 机器ID位长度
    private const int SequenceBits = 12; // 序列号位长度

    // 最大值(位运算计算)
    private const long MaxDataCenterId = (1L << DataCenterIdBits) - 1; // 31
    private const long MaxMachineId = (1L << MachineIdBits) - 1; // 31
    private const long MaxSequence = (1L << SequenceBits) - 1; // 4095

    // 位移量
    private const int MachineIdShift = SequenceBits; // 12
    private const int DataCenterIdShift = SequenceBits + MachineIdBits; // 17
    private const int TimestampShift = SequenceBits + MachineIdBits + DataCenterIdBits; // 22
    #endregion

    #region 私有字段
    private readonly long _dataCenterId; // 数据中心ID
    private readonly long _machineId; // 机器ID
    private long _lastTimestamp = -1L; // 上一次生成ID的时间戳
    private long _sequence = 0L; // 当前毫秒内的序列号
    private readonly object _lockObj = new(); // 锁对象(保证线程安全)
    #endregion

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="dataCenterId">数据中心ID(0-31)</param>
    /// <param name="machineId">机器ID(0-31)</param>
    /// <exception cref="ArgumentException">参数超出范围时抛出</exception>
    public SnowflakeIdGenerator(long dataCenterId, long machineId)
    {
        // 校验参数合法性
        if (dataCenterId < 0 || dataCenterId > MaxDataCenterId)
        {
            throw new ArgumentException($"数据中心ID必须在0-{MaxDataCenterId}之间", nameof(dataCenterId));
        }
        if (machineId < 0 || machineId > MaxMachineId)
        {
            throw new ArgumentException($"机器ID必须在0-{MaxMachineId}之间", nameof(machineId));
        }

        _dataCenterId = dataCenterId;
        _machineId = machineId;
    }

    /// <summary>
    /// 生成下一个雪花ID
    /// </summary>
    /// <returns>全局唯一的雪花ID</returns>
    /// <exception cref="Exception">时钟回拨超出容忍范围时抛出</exception>
    public long NextId()
    {
        lock (_lockObj) // 加锁保证多线程安全
        {
            // 1. 获取当前时间戳(毫秒)
            long currentTimestamp = GetCurrentTimestamp();

            // 2. 处理时钟回拨问题
            if (currentTimestamp < _lastTimestamp)
            {
                long timeDiff = _lastTimestamp - currentTimestamp;
                // 容忍500ms内的时钟回拨(等待直到时间追上)
                if (timeDiff <= 500)
                {
                    // 自旋等待,直到时间戳追上上次的时间戳
                    while (currentTimestamp < _lastTimestamp)
                    {
                        currentTimestamp = GetCurrentTimestamp();
                    }
                }
                else
                {
                    // 超出容忍范围,抛出异常(避免ID重复)
                    throw new Exception($"时钟回拨异常!当前时间戳:{currentTimestamp},上次时间戳:{_lastTimestamp},差值:{timeDiff}ms");
                }
            }

            // 3. 处理同一毫秒内的序列号
            if (currentTimestamp == _lastTimestamp)
            {
                // 序列号自增,超过最大值则等待下一毫秒
                _sequence = (_sequence + 1) & MaxSequence;
                if (_sequence == 0)
                {
                    // 自旋等待下一毫秒
                    while (currentTimestamp <= _lastTimestamp)
                    {
                        currentTimestamp = GetCurrentTimestamp();
                    }
                }
            }
            else
            {
                // 新的毫秒,序列号重置为0
                _sequence = 0;
            }

            // 4. 更新上次时间戳
            _lastTimestamp = currentTimestamp;

            // 5. 拼接各部分生成最终ID
            return ((currentTimestamp - Epoch) << TimestampShift) // 时间戳部分
                   | (_dataCenterId << DataCenterIdShift) // 数据中心ID部分
                   | (_machineId << MachineIdShift) // 机器ID部分
                   | _sequence; // 序列号部分
        }
    }

    /// <summary>
    /// 获取当前UTC时间戳(毫秒)
    /// </summary>
    /// <returns>毫秒级时间戳</returns>
    private long GetCurrentTimestamp()
    {
        return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
    }

    #region 便捷方法(可选)
    /// <summary>
    /// 生成雪花ID的字符串形式
    /// </summary>
    /// <returns>ID字符串</returns>
    public string NextIdString()
    {
        return NextId().ToString();
    }

    /// <summary>
    /// 解析雪花ID,返回各部分信息(用于调试)
    /// </summary>
    /// <param name="id">雪花ID</param>
    /// <returns>解析后的信息</returns>
    public (DateTime GenerateTime, long DataCenterId, long MachineId, long Sequence) ParseId(long id)
    {
        long timestamp = (id >> TimestampShift) + Epoch;
        DateTime generateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(timestamp);
        long dataCenterId = (id >> DataCenterIdShift) & MaxDataCenterId;
        long machineId = (id >> MachineIdShift) & MaxMachineId;
        long sequence = id & MaxSequence;

        return (generateTime.ToLocalTime(), dataCenterId, machineId, sequence);
    }
    #endregion
}

三、使用示例

csharp 复制代码
class Program
{
    static void Main(string[] args)
    {
        // 初始化生成器(数据中心ID=1,机器ID=1,根据实际部署环境调整)
        var idGenerator = new SnowflakeIdGenerator(1, 1);

        // 单线程生成ID
        long id1 = idGenerator.NextId();
        Console.WriteLine($"生成的雪花ID:{id1}");

        // 解析ID(调试用)
        var parseResult = idGenerator.ParseId(id1);
        Console.WriteLine($"生成时间:{parseResult.GenerateTime}");
        Console.WriteLine($"数据中心ID:{parseResult.DataCenterId}");
        Console.WriteLine($"机器ID:{parseResult.MachineId}");
        Console.WriteLine($"序列号:{parseResult.Sequence}");

        // 多线程测试(验证线程安全)
        Parallel.For(0, 1000, i =>
        {
            long id = idGenerator.NextId();
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}生成ID:{id}");
        });
    }
}

四、关键说明

  1. 起始时间戳(Epoch):建议设置为项目上线前的时间(如示例中的2024-01-01),可延长ID使用周期(默认能用到2093年)。
  2. 机器/数据中心ID:部署时需保证每个节点的(数据中心ID+机器ID)唯一(比如通过配置文件/环境变量设置),避免不同节点生成重复ID。
  3. 时钟回拨处理:代码中容忍500ms内的时钟回拨(自旋等待),超出则抛异常,可根据业务调整容忍阈值。
  4. 性能:单节点每秒可生成约400万+ ID(1ms生成4096个,1秒=1000ms → 4096*1000=4,096,000),满足绝大多数业务场景。

总结

  1. 雪花ID核心是通过位运算将时间戳、机器ID、序列号拼接成64位唯一ID,保证全局唯一且趋势递增;
  2. 实现时必须保证线程安全 (加锁)和时钟回拨检测(避免ID重复);
  3. 使用时需保证每个节点的机器/数据中心ID唯一,起始时间戳建议根据项目实际调整。

这个实现可直接集成到你的.NET分库分表项目中,作为分布式主键生成方案。

本文使用 文章同步助手 同步

相关推荐
毕设源码-郭学长17 小时前
【开题答辩全过程】以 基于springboot 的豪华婚车租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Loo国昌18 小时前
深入理解 FastAPI:Python高性能API框架的完整指南
开发语言·人工智能·后端·python·langchain·fastapi
码农水水20 小时前
米哈游Java面试被问:机器学习模型的在线服务和A/B测试
java·开发语言·数据库·spring boot·后端·机器学习·word
计算机学姐21 小时前
基于SpringBoot的美食分享交流平台
java·spring boot·后端·spring·java-ee·intellij-idea·美食
源代码•宸1 天前
Leetcode—746. 使用最小花费爬楼梯【简单】
后端·算法·leetcode·职场和发展·golang·记忆化搜索·动规
毕设源码-朱学姐1 天前
【开题答辩全过程】以 基于Django框架中山社区社会补助系统为例,包含答辩的问题和答案
后端·python·django
J_liaty1 天前
分库分表深度解析
后端
AIFQuant1 天前
如何通过股票数据 API 计算 RSI、MACD 与移动平均线MA
大数据·后端·python·金融·restful
x70x801 天前
Go中nil的使用
开发语言·后端·golang
REDcker1 天前
libwebsockets库原理详解
c++·后端·websocket·libwebsockets