直接用 SnowflakeIdGenerator 类容易出错,是因为未满足其核心前提:时间戳必须单调递增、workerId 在集群中必须全局唯一且稳定;常见错误包括 workerId 范围越界、时钟回拨未处理、sequence 非线程安全、实例非单例导致状态重置、时间戳误用 DateTime.Now.Millisecond 等。

为什么直接用 SnowflakeIdGenerator 类容易出错
很多人一上来就抄一个“C# Snowflake 实现”,结果在高并发或时钟回拨场景下生成重复 ID。根本原因是没理解雪花算法对时间戳和机器 ID 的强依赖——timestamp 必须单调递增,workerId 在集群中必须全局唯一,且不能靠随机数或 GUID 临时凑合。
常见错误现象:System.ArgumentException: WorkerId must be between 0 and 1023、ID 时间部分倒退、同一毫秒内生成超 4096 个 ID 导致溢出、多实例部署时 ID 冲突。
- 别用
DateTime.Now.Millisecond算时间戳——精度不够,且不保证单调;改用Stopwatch.GetTimestamp()或基于DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()+ 自增补偿 -
workerId不能硬编码,建议从配置中心(如 Consul、Nacos)或启动参数读取,避免本地调试和生产环境值混淆 - 序列号(
sequence)必须是线程安全的整型(如Interlocked.Increment),不能用普通int++
如何安全初始化 SnowflakeIdGenerator 实例
单例复用是底线。每次 new 一个新实例,等于重置了 lastTimestamp 和 sequence,极易触发重复 ID。尤其在 ASP.NET Core 中,千万别把它注册成 Scoped 或 Transient。
推荐方式:用 Singleton + 构造时校验:
public class SnowflakeIdGenerator
{
private readonly long _workerId;
private readonly long _datacenterId;
private long _lastTimestamp = -1L;
private long _sequence = 0L;
<pre class="brush:php;toolbar:false;">public SnowflakeIdGenerator(long workerId, long datacenterId = 0)
{
if (workerId 0x3FF) throw new ArgumentException("WorkerId must be between 0 and 1023");
if (datacenterId 0x1F) throw new ArgumentException("DatacenterId must be between 0 and 31");
_workerId = workerId <p>}</p>
- ASP.NET Core 启动时注入:
services.AddSingleton<snowflakeidgenerator>(sp => new SnowflakeIdGenerator(workerId: GetWorkerIdFromConfig()));</snowflakeidgenerator> - 本地开发可设
workerId = Environment.MachineName.GetHashCode() & 0x3FF,但上线前必须换为稳定分配方案 - 务必在构造函数里做参数范围检查,避免运行时静默截断
生成 ID 时怎么处理时钟回拨
服务器 NTP 同步、虚拟机暂停、手动改系统时间都会导致 currentTimestamp 。标准做法不是抛异常,而是阻塞等待(最多 5ms),超过则抛 <code>InvalidOperationException 并告警——因为持续回拨说明基础设施异常,不该继续发 ID。
关键逻辑在 NextId() 内部:
private long NextId()
{
var currentTimestamp = CurrentTimeMillis();
if (currentTimestamp 5) throw new InvalidOperationException($"Clock moved backwards: {offset} ms");
<pre class="brush:php;toolbar:false;"> Thread.Sleep((int)offset);
currentTimestamp = CurrentTimeMillis();
if (currentTimestamp <p>}</p>
-
Twepoch建议设为项目上线时间戳(如1609459200000L对应 2021-01-01),别用默认的 Twitter 时间,避免高位全零浪费 -
WaitNextMillis()必须用 while 循环轮询,不能只 sleep 1ms——毫秒级时间精度下,sleep 不精确 - 日志里记录每次回拨事件,方便后续排查时钟源问题
生成的 long ID 怎么安全转成字符串或数据库主键
直接存 long 没问题,但前端 JS 处理会丢失精度(JS Number 最大安全整数是 2^53 - 1,而雪花 ID 可达 2^63 - 1)。所以返回给前端必须转 string,数据库若用 MySQL,推荐 BIGINT UNSIGNED 或 VARCHAR(20)。
- 不要用
.ToString()后再拼接前缀——破坏全局唯一性,且增加索引长度;保持原始数值语义 - EF Core 映射时,用
[Column(TypeName = "bigint unsigned")](MySQL)或[DatabaseGenerated(DatabaseGeneratedOption.None)]配合手动赋值 - 如果必须兼容旧系统要求 32 位 int 主键,说明你根本不该用雪花算法——换
Guid.NewGuid()或数据库自增更合适
最常被忽略的一点:不同服务共用同一套 workerId 分配逻辑时,没做跨 DC 容灾隔离。比如 A 机房挂了,B 机房顶上来的实例如果沿用原 workerId,可能因时间未同步造成 ID 冲突。实际部署中,workerId 应与物理节点绑定,而非服务名或进程 ID。











