你希望在.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}");
});
}
}
四、关键说明
- 起始时间戳(Epoch):建议设置为项目上线前的时间(如示例中的2024-01-01),可延长ID使用周期(默认能用到2093年)。
- 机器/数据中心ID:部署时需保证每个节点的(数据中心ID+机器ID)唯一(比如通过配置文件/环境变量设置),避免不同节点生成重复ID。
- 时钟回拨处理:代码中容忍500ms内的时钟回拨(自旋等待),超出则抛异常,可根据业务调整容忍阈值。
- 性能:单节点每秒可生成约400万+ ID(1ms生成4096个,1秒=1000ms → 4096*1000=4,096,000),满足绝大多数业务场景。
总结
- 雪花ID核心是通过位运算将时间戳、机器ID、序列号拼接成64位唯一ID,保证全局唯一且趋势递增;
- 实现时必须保证线程安全 (加锁)和时钟回拨检测(避免ID重复);
- 使用时需保证每个节点的机器/数据中心ID唯一,起始时间戳建议根据项目实际调整。
这个实现可直接集成到你的.NET分库分表项目中,作为分布式主键生成方案。
本文使用 文章同步助手 同步