<p>结论:C# 中生成雪花ID应优先使用SnowflakeId(.NET 8.0+)或IdGen等成熟库,避免手写;手写易在时钟回拨、worker ID分配、sequence溢出等关键点出错,生产环境风险极高。</p>

直接说结论:C# 里用雪花算法生成 ID,别手写,优先用 SnowflakeId(来自 Microsoft.Extensions.TimeProvider 8.0+)或成熟库如 IdGen、TwitterSnowflake;手写极易出错,尤其在时钟回拨、多实例部署、worker ID 分配上。
为什么不能直接照抄网上的“10行雪花算法”
网上大量所谓“C# 雪花算法实现”只处理了位运算逻辑,却完全忽略生产环境必须面对的几个硬伤:
- 没做时钟回拨检测 —— 服务器时间被 NTP 校正后,
GetTimestamp()返回值变小,会导致 ID 重复或阻塞 - worker ID 硬编码或靠随机数生成 —— 多进程/多容器场景下极易冲突,ID 不唯一
- 没考虑
sequence溢出重置时机 —— 同一毫秒内超 4096 次调用就直接抛异常或静默截断 - 依赖
DateTime.Now而非单调时钟 —— 在虚拟机休眠、系统暂停后可能跳变
推荐方案:用 IdGen 库(.NET Standard 2.0+ 兼容性最好)
IdGen 是社区验证最久、文档最清晰的 C# 雪花兼容实现,支持自定义 epoch、ID 长度、worker ID 分配策略,且内置时钟保护。
安装:
dotnet add package IdGen
基础用法:
var options = new IdGenOptions(1); // worker ID = 1 var idgen = new IdGenerator(options); long id = idgen.CreateId(); // 返回 long 类型 ID
关键注意点:
-
IdGenOptions构造时传入的workerId必须全局唯一 —— 推荐从配置中心读取,或用 Kubernetes Downward API 注入POD_NAME做哈希 - 默认 epoch 是
2020-01-01,若需兼容老系统,显式设置options.Epoch = new DateTime(2010, 1, 1, 0, 0, 0, DateTimeKind.Utc); - 不建议在 Web API 的每个请求里 new 一个
IdGenerator—— 它是线程安全的,应注册为 Singleton
如果必须手写:至少守住这三条底线
仅限学习或极简嵌入场景。以下是最简可用骨架(省略日志、配置注入等):
public class SimpleSnowflake
{
private const long TWEPOCH = 1288834974657L; // 2010-11-04
private const int WORKER_ID_BITS = 5;
private const int DATA_CENTER_ID_BITS = 5;
private const int SEQUENCE_BITS = 12;
private const long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
private readonly long _workerId;
private long _sequence = 0L;
private long _lastTimestamp = -1L;
<pre class='brush:php;toolbar:false;'>public SimpleSnowflake(long workerId)
{
if (workerId > MAX_WORKER_ID || workerId < 0)
throw new ArgumentException($"worker Id can't be greater than {MAX_WORKER_ID} or less than 0");
_workerId = workerId;
}
public long NextId()
{
var timestamp = CurrentTime();
if (timestamp < _lastTimestamp)
throw new InvalidOperationException($"Clock moved backwards. Refusing to generate id for {_lastTimestamp - timestamp} milliseconds");
if (_lastTimestamp == timestamp)
{
_sequence = (_sequence + 1) & 0xfff;
if (_sequence == 0)
timestamp = WaitNextMillis(_lastTimestamp);
}
else _sequence = 0;
_lastTimestamp = timestamp;
return ((timestamp - TWEPOCH) << 22) | (_workerId << 17) | _sequence;
}
private long CurrentTime() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private long WaitNextMillis(long lastTimestamp)
{
var timestamp = CurrentTime();
while (timestamp <= lastTimestamp) timestamp = CurrentTime();
return timestamp;
}}
必须检查:
- 构造函数传入的
workerId是否真的不重复 —— 本地测试用 1,上线必须动态分配 -
CurrentTime()不能替换成DateTime.Now,必须用DateTimeOffset.UtcNow或TimeProvider.GetUtcNow()(.NET 8+) -
WaitNextMillis是阻塞等待,高并发下会拖慢吞吐 —— 生产环境应改用拒绝(抛异常)或降级(返回 Guid)
真正难的从来不是位运算,而是让多个服务实例在分布式环境下协同生成不重复、趋势递增、无时钟依赖的 ID。worker ID 的分发机制、时钟漂移的容忍策略、以及 ID 解析(反解时间戳/机器号)能力,才是落地时反复卡住的地方。


















