c# 在 C# 中实现一个简单的分布式ID生成器(雪花算法)

来源:这里教程网 时间:2026-02-21 17:41:28 作者:

为什么直接用
SnowflakeIdGenerator
类容易出错

很多人照着网上的 C# 雪花算法实现抄一个

SnowflakeIdGenerator
类,运行起来 ID 看似递增,但一上多节点就重复——根本原因是没处理好 **机器 ID(workerId)和数据中心 ID(datacenterId)的全局唯一分配**。雪花算法要求同一毫秒内,不同节点的
workerId
必须不重叠,否则
timestamp + workerId + sequence
组合就可能碰撞。

常见错误包括:

硬编码
workerId = 1
,本地跑得通,部署到 3 台服务器全设成 1,必然冲突
用 IP 哈希取模算
workerId
,但 IPv4 地址段有限,哈希后碰撞概率高
依赖配置文件手动填
workerId
,运维漏配或填错,服务启动失败或 ID 冲突静默发生

ZooKeeper
Redis
自动分配
workerId
更可靠

生产环境推荐用中心化协调服务动态分配,避免人工干预。Redis 是多数团队已有组件,接入成本低:

每个实例启动时,用 Lua 脚本在 Redis 中原子性地申请一个未被占用的
workerId
(比如从 0–1023 范围取)
成功后写入临时 key(带 TTL),并监听该 key 过期或主动释放 若申请不到可用 ID,应阻塞等待或快速失败,不能降级为随机数——那就不叫雪花算法了

示例 Lua 脚本逻辑(用于

redis-cli --eval
):

local used = redis.call('smembers', 'snowflake:used_worker_ids')
local all = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
for _, id in ipairs(all) do
  local is_used = false
  for _, u in ipairs(used) do
    if tonumber(u) == id then is_used = true; break end
  end
  if not is_used then
    redis.call('sadd', 'snowflake:used_worker_ids', id)
    redis.call('setex', 'snowflake:worker:' .. id, 300, 'alive')
    return id
  end
end
return -1

DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
DateTime.Now
更安全

雪花算法依赖时间戳左移,一旦系统时钟回拨(NTP 校正、虚拟机休眠恢复),

DateTime.Now
可能跳变,导致生成重复 ID 或序列号溢出。C# 中应统一用 UTC 时间戳:

DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
返回 long,精度毫秒,无时区歧义
不要用
DateTime.Now.Ticks / 10000
,它依赖本地时区且易受夏令时影响
建议在 ID 生成器内部缓存上一次时间戳,若新时间 ≤ 上次值,则用
sequence++
;若差值 > 5s,可 panic 或抛异常——说明时钟严重异常

序列号
sequence
溢出时必须阻塞等待,不能重置

标准雪花算法中,序列号占 12 bit(0–4095),意味着每毫秒最多生成 4096 个 ID。如果业务峰值超过该速率,常见错误是“超了就清零”,这会破坏单调递增性,且在分布式下极易撞 ID。

正确做法:当
sequence == 4095
且当前毫秒未变时,线程 sleep 到下一毫秒再继续
sleep 时间不宜用
Thread.Sleep(1)
,而应计算目标时间戳后调用
SpinWait.SpinUntil
或基于
Stopwatch
的 busy-wait,减少上下文切换开销
注意:.NET 6+ 中
System.TimeProvider
可用于测试时模拟时间推进,方便压测序列号阻塞逻辑

简单阻塞示意(非完整实现):

while (currentTimestamp == _lastTimestamp)
{
    currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    if (currentTimestamp == _lastTimestamp)
    {
        // 自旋等待,直到下一毫秒
        while (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() <= currentTimestamp) { }
        currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    }
}

时间戳和序列号的协同边界非常敏感,任何想“绕过限制”的优化(比如扩展 sequence 位数、改用 hybrid logical clock)都会脱离雪花协议,下游系统可能无法识别。保持原生 64 位结构,才是兼容性和可维护性的底线。

相关推荐