Sparkdo.IdGeneration.Snowflake
3.0.0-preview.1
dotnet add package Sparkdo.IdGeneration.Snowflake --version 3.0.0-preview.1
NuGet\Install-Package Sparkdo.IdGeneration.Snowflake -Version 3.0.0-preview.1
<PackageReference Include="Sparkdo.IdGeneration.Snowflake" Version="3.0.0-preview.1" />
<PackageVersion Include="Sparkdo.IdGeneration.Snowflake" Version="3.0.0-preview.1" />
<PackageReference Include="Sparkdo.IdGeneration.Snowflake" />
paket add Sparkdo.IdGeneration.Snowflake --version 3.0.0-preview.1
#r "nuget: Sparkdo.IdGeneration.Snowflake, 3.0.0-preview.1"
#:package Sparkdo.IdGeneration.Snowflake@3.0.0-preview.1
#addin nuget:?package=Sparkdo.IdGeneration.Snowflake&version=3.0.0-preview.1&prerelease
#tool nuget:?package=Sparkdo.IdGeneration.Snowflake&version=3.0.0-preview.1&prerelease
Sparkdo.IdGeneration.Snowflake
Sparkdo.IdGeneration.Snowflake 提供 long Id 的 Snowflake 运行时:41-bit 相对 epoch 的毫秒偏移 + 10-bit worker id + 12-bit sequence,worker id 经 IDistributedLock 租约分配。生成路径同步、单锁、无分配;所有前提不成立一律 fail closed(中文异常消息),不静默降质、不隐式重连。
依赖:Sparkdo.IdGeneration(含 Abstractions)与 Sparkdo.DistributedLocking.Abstractions(只用契约,不选 provider——你需要自行注册一个符合部署范围的 IDistributedLock,例如本仓库 Sparkdo.DistributedLocking.Redis)。
快速开始
builder.Services.AddSparkdoRedis(o => o.Configuration = "localhost:6379"); // redis 域:连接配置
builder.Services.AddSparkdoRedisDistributedLock(); // 跨进程锁 provider
builder.Services.AddSparkdoIdGeneration();
builder.Services.AddSparkdoIdGenerationSnowflake();
var app = builder.Build();
// 进入服务前执行一次;失败就让启动失败。可传入宿主的停止令牌(如 IHostApplicationLifetime.ApplicationStopping)
await app.Services.GetRequiredService<SnowflakeWorkerStartupTask>().ExecuteAsync();
var id = app.Services.GetRequiredService<ILongIdGenerator>().Create(); // 例如 4194332672
AddSparkdoIdGenerationSnowflake() 也 TryAdd IIdGeneratorProvider,所以单装本包时经 IIdGeneratorProvider.Get<long>() 解析同样可用;与 AddSparkdoIdGeneration() 共同安装时两侧各注册一次,调用顺序不影响结果。
非 DI 用法:
using Microsoft.Extensions.Options;
using Sparkdo.IdGeneration;
var options = Options.Create(new SnowflakeIdGeneratorOptions());
await using var state = new SnowflakeIdState(myWorkerIdProvider, options);
await state.EnsureLeaseAsync();
var generator = new SnowflakeLongIdGenerator(state);
var id = generator.Create();
AddSparkdoIdGenerationSnowflake() 注册了什么
| 服务 | 实现 | Lifetime | 注册方式 |
|---|---|---|---|
ILongIdGenerator |
SnowflakeLongIdGenerator |
Singleton | TryAdd |
IIdGenerator<long> |
SnowflakeLongIdGenerator |
Singleton | TryAdd |
ISnowflakeWorkerIdProvider |
DistributedLockSnowflakeWorkerIdProvider |
Singleton | TryAdd |
SnowflakeIdState |
自身 | Singleton,IAsyncDisposable |
TryAdd |
SnowflakeWorkerStartupTask |
自身 | Singleton | TryAdd |
IIdGeneratorProvider |
DefaultIdGeneratorProvider |
Transient | TryAdd |
SnowflakeIdGeneratorOptions |
— | — | AddOptions<T>()(不绑定任何配置源) |
- 全部
TryAdd:宿主先注册的实现优先,重复调用幂等。想换 worker 来源(数据库表、配置固定值等),在本方法之前注册自己的ISnowflakeWorkerIdProvider即可。 - 本方法不注册
IDistributedLock。缺失时ExecuteAsync()抛InvalidOperationException: Unable to resolve service for type 'Sparkdo.DistributedLocking.IDistributedLock' while attempting to activate 'Sparkdo.IdGeneration.DistributedLockSnowflakeWorkerIdProvider'. - 一个容器里只应有一个
SnowflakeIdState:它是 sequence、逻辑时间与 lease 的唯一持有者。同一 state 可以开多个SnowflakeLongIdGenerator门面,它们共享同一 sequence(有测试钉住)。
公共类型逐成员
| 类型 | 成员 | 语义 |
|---|---|---|
SnowflakeIdState |
ctor(ISnowflakeWorkerIdProvider, IOptions<SnowflakeIdGeneratorOptions>) |
公共构造;internal 重载可注入 TimeProvider(仅测试程序集) |
EnsureLeaseAsync(CancellationToken) : ValueTask |
取并持有唯一 lease。幂等(两次调用只请求一次 worker provider);并发调用只留一个 lease、其余句柄释放;已丢失的 lease 直接抛;Epoch 或 lease/renewal 配置非法时在动作前抛(worker provider 尝试数为 0) |
|
Create() : long |
同步生成;要求已持有有效 lease(见「失败矩阵」) | |
DisposeAsync() |
释放持有的 lease(归还锁),幂等。释放后再调 Create() 或 EnsureLeaseAsync() 都抛 ObjectDisposedException |
|
SnowflakeLongIdGenerator |
ctor(SnowflakeIdState)、Create() : long |
ILongIdGenerator 门面,转调 state |
SnowflakeWorkerStartupTask |
ctor(SnowflakeIdState)、ExecuteAsync(CancellationToken) : ValueTask |
转调 state.EnsureLeaseAsync()。由宿主在进入服务前调用;本包不引入 Hosting 依赖,不自动执行 |
ISnowflakeWorkerIdProvider |
AcquireAsync(CancellationToken) : ValueTask<SnowflakeWorkerLease> |
worker id 来源 SPI。必须保证返回的 worker id 在本部署范围内不与其他进程同时有效 |
SnowflakeWorkerLease |
ctor(int workerId, IAsyncDisposable resource, CancellationToken lostToken = default) |
workerId 必须落在 0..1023,否则 ArgumentOutOfRangeException;resource 是释放边界(DisposeAsync 幂等、只释放一次);lostToken 必须是可取消的令牌(失锁通知是 fail-closed 体系的组成部分)——默认值/CancellationToken.None 构造即抛 ArgumentException;确有「永不失去租约」的场景(宿主静态分配 worker id)显式传入 SnowflakeWorkerLease.NeverLostToken |
NeverLostToken(静态) |
显式的「永不失去租约」令牌:可取消但永不触发,与「忘记传 token」在构造期即可区分 | |
WorkerId、LostToken、DisposeAsync() |
只读快照 | |
DistributedLockSnowflakeWorkerIdProvider |
ctor(IDistributedLock, IOptions<SnowflakeIdGeneratorOptions>)、AcquireAsync(ct) |
默认实现,见下节 |
SnowflakeIdGeneratorOptions |
四个属性 | 见「配置」 |
默认 worker id 分配:DistributedLockSnowflakeWorkerIdProvider
AcquireAsync 的顺序:
- 校验配置:
WorkerLeaseDuration > 0;0 < WorkerRenewalInterval < WorkerLeaseDuration。违反即抛,且不触碰锁。 - 校验锁的协调强度:读取
IDistributedLock.Provider。若IsProcessLocal == true且未设置AllowProcessLocalWorkerLease,在扫描前拒绝:Snowflake worker lease 需要跨进程协调:当前 IDistributedLock provider 'in-memory' 仅进程内互斥。请配置具备真实租约语义的分布式锁 provider,或仅在单进程场景显式设置 SnowflakeIdGeneratorOptions.AllowProcessLocalWorkerLease。 - 零等待扫描
0..1023:锁名Sparkdo.IdGeneration.Snowflake.Worker.{workerId},每次尝试带WaitTimeout = TimeSpan.Zero、LeaseDuration = WorkerLeaseDuration、AutoRenewalInterval = WorkerRenewalInterval。取到第一个未失锁的句柄即返回(实测最坏 1024 次尝试,首个可用通常第 1 次命中)。 - 已失锁的句柄被释放后继续下一个;全部占用抛
无法获取 Snowflake worker lease:0..1023 均不可用。 cancellationToken在每次迭代前检查:启动前已取消则零尝试抛OperationCanceledException。
含义:worker id 是抢占式租约而非固定身份。进程重启后可能拿到不同 worker id(锁名扫描顺序固定,但占用情况会变);不要依赖「某台机器恒定等于某个 worker id」,需要稳定映射请实现自己的 ISnowflakeWorkerIdProvider。
部署边界:进程内 IDistributedLock 无法跨 Host/跨机器协调,默认配置下被直接拒绝。生产必须使用具备真实租约、续租与失锁通知语义的 provider(如 Sparkdo.DistributedLocking.Redis),否则没有全局唯一性保证。
配置:SnowflakeIdGeneratorOptions
| 属性 | 默认 | 约束 | 用途 |
|---|---|---|---|
Epoch |
2025-01-01T00:00:00Z |
不得晚于当前 UTC;偏移须落在 41-bit 内 | 41-bit 时间戳的零点 |
WorkerLeaseDuration |
00:00:30 |
必须 > 0 | 传给锁的 LeaseDuration |
WorkerRenewalInterval |
00:00:10 |
必须 > 0 且严格小于 WorkerLeaseDuration |
传给锁的 AutoRenewalInterval |
AllowProcessLocalWorkerLease |
false |
— | 仅单进程/开发场景显式放行进程内锁 |
builder.Services.Configure<SnowflakeIdGeneratorOptions>(o =>
{
o.WorkerLeaseDuration = TimeSpan.FromSeconds(15);
o.WorkerRenewalInterval = TimeSpan.FromSeconds(5);
});
builder.Services.Configure<SnowflakeIdGeneratorOptions>(
builder.Configuration.GetSection("Sparkdo:IdGeneration:Snowflake"));
约束在 SnowflakeIdState.EnsureLeaseAsync 与 worker provider 两侧都校验(消息一致),所以走 DI、走非 DI 构造、走自定义 provider 都拦得住。Epoch 非法(晚于当前时间)在取 lease 时抛,worker provider 不会被调用。
投产后不要改 Epoch:同一数值的 Id 会解出不同时刻。需要扩期请新增一套并让历史数据带各自的 epoch 元数据。
语义与失败矩阵
Create() 的输入输出(实测):
| 状态/事件 | 结果 |
|---|---|
| 未执行 startup(lease 尚未获取) | InvalidOperationException: Snowflake worker lease 尚未获取;请在应用进入服务前执行 SnowflakeWorkerStartupTask.ExecuteAsync。 |
lease 已丢失(LostToken 被取消) |
InvalidOperationException: Snowflake worker lease 已丢失,不能继续生成 Id。 |
| lease 在获取的瞬间已丢失 | InvalidOperationException: Snowflake worker lease 在获取时已经丢失。 |
| 物理时钟回拨 | InvalidOperationException: Snowflake 物理时钟回拨:current={当前}, last={已观测最大}。 回退不会自愈,需等时钟追平 last(实测回到同一高值可继续,仍低 1ms 依旧抛) |
Epoch 晚于当前 UTC |
InvalidOperationException: Snowflake epoch 晚于当前 UTC 时间。(Create() 路径)/Snowflake Epoch 不能晚于当前 UTC 时间。(EnsureLeaseAsync 校验) |
| 偏移超出 41-bit | InvalidOperationException: Snowflake timestamp 超出 41-bit 范围。(默认 epoch 下界为 2025-01-01,上界约 2094-09-07T15:47:35.551Z) |
| state 已释放 | ObjectDisposedException |
| 同毫秒连续调用 | sequence 递增:第二个 Id = 第一个 + 1(实测) |
| 时钟前进 1ms | 增量 = (1 << 22) - 1 = 4194303(实测,含 sequence 归零) |
| 同毫秒超过 4096 次 | 逻辑时间戳 +1、sequence 归零;不阻塞等待物理时钟(实测时钟停在 1001ms 时逻辑已到 1002ms) |
| 并发 20 万次 | 20 万个互异值(实测,单锁串行化) |
| worker 7、时钟 = epoch + 1000ms 的第一个 Id | 4194332672 = 0x00000000FA007000(实测,ts=1000, worker=7, seq=0) |
三个位段填满正好是 long.MaxValue,因此产出的 Id 恒为非负 long,无需无符号处理或位翻转。
用法配方
反解 Id
long id = generator.Create();
var elapsedMs = id >> 22; // 相对 Epoch 的毫秒偏移(逻辑时间)
var workerId = (id >> 12) & 0x3FF; // 0..1023
var sequence = (int)(id & 0xFFF); // 0..4095
var createdAt = options.Value.Epoch.AddMilliseconds(elapsedMs);
按 worker 统计来源、或按 id >> 22 做时间分桶路由,都可以直接用这些位移,无需字符串转换。
顺序分页
Snowflake Id 单调非降,ORDER BY id 即按创建顺序;但注意 sequence 溢出时逻辑时间会领先真实时间,跨实例比较时 worker id 段会让同一毫秒内不同实例的 Id 交错——所以「按 Id 排序」在跨实例场景只保证不重复、不保证严格的全局创建顺序(需要严格全序请只按 id >> 22 加数据库时间列排序)。
接入固定 worker id
public sealed class FixedWorkerIdProvider(int workerId) : ISnowflakeWorkerIdProvider, IDisposable
{
private readonly CancellationTokenSource leaseLost = new();
public ValueTask<SnowflakeWorkerLease> AcquireAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.FromResult(new SnowflakeWorkerLease(
workerId,
resource: EmptyResource.Instance, // 无锁资源时也要给一个可 DisposeAsync 的对象
lostToken: leaseLost.Token)); // 编排判定下线时取消它
}
public void Dispose() => leaseLost.Dispose();
private sealed class EmptyResource : IAsyncDisposable
{
public static readonly EmptyResource Instance = new();
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
services.AddSingleton<ISnowflakeWorkerIdProvider>(new FixedWorkerIdProvider(configuredWorkerId));
services.AddSparkdoIdGenerationSnowflake(); // TryAdd 让位:上面已注册,默认锁 provider 不会覆盖
外部编排(K8s StatefulSet 序号、Consul 注册表等)保证 worker id 不冲突时,可以这样绕开锁扫描。此时 lostToken 就是你的失锁通知通道:编排系统判定该实例应下线时取消它,Create() 随即 fail closed。
单进程 / 本地开发放行进程内锁
builder.Services.AddSparkdoDistributedLocking(); // in-memory provider
if (builder.Environment.IsDevelopment())
{
builder.Services.Configure<SnowflakeIdGeneratorOptions>(o => o.AllowProcessLocalWorkerLease = true);
}
该开关只能用于确定只有一个进程的场景;多实例同时开启会静默破坏唯一性(这正是默认 fail-closed 的原因)。
关闭时归还 lease
await using var provider = services.BuildServiceProvider();
SnowflakeIdState 只实现 IAsyncDisposable,因此同步 provider.Dispose() 会抛 'Sparkdo.IdGeneration.SnowflakeIdState' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.,此时 worker lease 不会释放(实测句柄释放计数为 0;DisposeAsync 为 1)。凡是自己 services.BuildServiceProvider() 的场合(控制台、测试、组合根)都必须用 await using 或 await provider.DisposeAsync();由宿主框架托管容器时,请确认该宿主的关闭路径走异步释放。不要为了绕过这个异常去加 IDisposable 包装——那等于在关闭路径上做 sync-over-async。
常见坑
- 忘调
ExecuteAsync:首个Create()抛异常而不是返回 0 或随机值。把它接进启动流程,并让启动失败可见。 ExecuteAsync吞异常继续启动:lease 没到手就上线,等于把所有请求变成异常。要么让它 fail 掉启动,要么降级到不用longId。- 锁没先注册:
AddSparkdoIdGenerationSnowflake()不会替你注册IDistributedLock。 - 生产用进程内锁 +
AllowProcessLocalWorkerLease = true:多实例都拿到 worker 0,Id 直接重复。默认拒绝是为了保护你。 WorkerRenewalInterval >= WorkerLeaseDuration:配置错误在取 lease 时就抛;相等也不允许(必须严格小于),否则一次续租失败即失锁。LeaseDuration设得过短:锁后端抖动或续租线程延迟就会失锁,此后Create()永久抛——失锁不重连是设计。30s/10s 是留有 3 次续租余量的默认值。- 指望失锁后自动恢复:不会。重新
ExecuteAsync仍抛(lease 已丢失),必须重建 state(即重启进程)。 - 改
Epoch:历史 Id 解出的时间全部失真。 - 以为重启后 worker id 不变:租约是抢占式的,会变。
- 自己实现 provider 时不传
lostToken:SnowflakeWorkerLease构造函数要求令牌可取消,传默认值会直接抛ArgumentException(fail-closed,不会静默降级为 forever-valid);确有「永不失去租约」的场景(如宿主静态分配 worker id)必须显式传入SnowflakeWorkerLease.NeverLostToken,其余情况传真实的失锁通知 token。 - 同步释放容器:见「关闭时归还 lease」。
- 把逻辑时间戳当真实时间对账:超速时它会领先物理时钟;
id >> 22是「不早于」的下界语义,不是精确创建时刻。
边界
- 只做
long的 Snowflake 一种算法。号段、数据库自增、Redis 计数器、ULID/v7 单调尾都不提供。 - 不注册
IDistributedLock,不选择锁后端,不引入Microsoft.Extensions.Hosting,不自动执行启动任务。 - 不做时钟回拨的等待/容忍/补偿;不做跨实例的全局排序保证。
- 不提供 Id 解析 API(位移公式见配方)、不提供 worker id 查询接口、不提供 lease 状态指标(可观测性靠异常与锁侧指标)。
- 不承诺 Id 不可预测(worker id 与毫秒偏移明文可见)。
验证
dotnet test src/id-generation/Sparkdo.IdGeneration.slnx
dotnet pack src/id-generation/Sparkdo.IdGeneration.slnx -c Release
测试全离线:IDistributedLock 用 fake 替身、时钟用 TimeProvider 的 internal 重载注入,CI 不需要真实 Redis。
本文即包内 README.md(PackageReadmeFile,实测随 nupkg 发布),并与 Sparkdo.IdGeneration.Snowflake.xml 一起提供 IntelliSense。因此文中只以包名明文引用其他包,不使用相对路径链接。
安装
dotnet add package Sparkdo.IdGeneration.Snowflake
同时需要真实跨进程锁实现(例如 dotnet add package Sparkdo.DistributedLocking.Redis)。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Options (>= 10.0.8)
- Microsoft.Extensions.Primitives (>= 10.0.8)
- Sparkdo.DistributedLocking.Abstractions (>= 3.0.0-preview.1)
- Sparkdo.IdGeneration (>= 3.0.0-preview.1)
-
net11.0
- Sparkdo.DistributedLocking.Abstractions (>= 3.0.0-preview.1)
- Sparkdo.IdGeneration (>= 3.0.0-preview.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-preview.1 | 38 | 10/30/2026 |