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分库分表项目中,作为分布式主键生成方案。

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

相关推荐
乌日尼乐20 小时前
【Java】IO流完全指南
java·后端
Java水解20 小时前
Spring Boot:Java开发的神奇加速器(二)
spring boot·后端
sunbin20 小时前
Eclipse 数据空间组件-最小化运行环境MVD_of_EDC-10
后端
问道飞鱼20 小时前
【Rust编程】Cargo 工具详解:从基础到高级的完整指南
开发语言·后端·rust·cargo
无限大621 小时前
为什么"DevOps"能提高软件开发效率?——从开发到运维的融合
后端·程序员·架构
独自归家的兔21 小时前
基于 Doubao-Seedream-4.5 的单张图片生成后端接口实战
java·人工智能·spring boot·后端
禹中一只鱼21 小时前
【SpringBoot 配置文件】
java·spring boot·后端
神奇小汤圆21 小时前
MySQL性能的定海神针:万字长文带你盘透 innodb_buffer_pool_size
后端
该用户已不存在1 天前
拒绝无效内卷,这 7 个 JavaScript 库让代码更能打
前端·javascript·后端