Sparkdo.IdGeneration.Snowflake 3.0.0-preview.1

This is a prerelease version of Sparkdo.IdGeneration.Snowflake.
dotnet add package Sparkdo.IdGeneration.Snowflake --version 3.0.0-preview.1
                    
NuGet\Install-Package Sparkdo.IdGeneration.Snowflake -Version 3.0.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sparkdo.IdGeneration.Snowflake" Version="3.0.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sparkdo.IdGeneration.Snowflake" Version="3.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Sparkdo.IdGeneration.Snowflake" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sparkdo.IdGeneration.Snowflake --version 3.0.0-preview.1
                    
#r "nuget: Sparkdo.IdGeneration.Snowflake, 3.0.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sparkdo.IdGeneration.Snowflake@3.0.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sparkdo.IdGeneration.Snowflake&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sparkdo.IdGeneration.Snowflake&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Tool

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 的顺序:

  1. 校验配置:WorkerLeaseDuration > 0;0 < WorkerRenewalInterval < WorkerLeaseDuration。违反即抛,且不触碰锁。
  2. 校验锁的协调强度:读取 IDistributedLock.Provider。若 IsProcessLocal == true 且未设置 AllowProcessLocalWorkerLease,在扫描前拒绝: Snowflake worker lease 需要跨进程协调:当前 IDistributedLock provider 'in-memory' 仅进程内互斥。请配置具备真实租约语义的分布式锁 provider,或仅在单进程场景显式设置 SnowflakeIdGeneratorOptions.AllowProcessLocalWorkerLease。
  3. 零等待扫描 0..1023:锁名 Sparkdo.IdGeneration.Snowflake.Worker.{workerId},每次尝试带 WaitTimeout = TimeSpan.Zero、LeaseDuration = WorkerLeaseDuration、AutoRenewalInterval = WorkerRenewalInterval。取到第一个未失锁的句柄即返回(实测最坏 1024 次尝试,首个可用通常第 1 次命中)。
  4. 已失锁的句柄被释放后继续下一个;全部占用抛 无法获取 Snowflake worker lease:0..1023 均不可用。
  5. 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。

常见坑

  1. 忘调 ExecuteAsync:首个 Create() 抛异常而不是返回 0 或随机值。把它接进启动流程,并让启动失败可见。
  2. ExecuteAsync 吞异常继续启动:lease 没到手就上线,等于把所有请求变成异常。要么让它 fail 掉启动,要么降级到不用 long Id。
  3. 锁没先注册:AddSparkdoIdGenerationSnowflake() 不会替你注册 IDistributedLock。
  4. 生产用进程内锁 + AllowProcessLocalWorkerLease = true:多实例都拿到 worker 0,Id 直接重复。默认拒绝是为了保护你。
  5. WorkerRenewalInterval >= WorkerLeaseDuration:配置错误在取 lease 时就抛;相等也不允许(必须严格小于),否则一次续租失败即失锁。
  6. LeaseDuration 设得过短:锁后端抖动或续租线程延迟就会失锁,此后 Create() 永久抛——失锁不重连是设计。30s/10s 是留有 3 次续租余量的默认值。
  7. 指望失锁后自动恢复:不会。重新 ExecuteAsync 仍抛(lease 已丢失),必须重建 state(即重启进程)。
  8. 改 Epoch:历史 Id 解出的时间全部失真。
  9. 以为重启后 worker id 不变:租约是抢占式的,会变。
  10. 自己实现 provider 时不传 lostToken:SnowflakeWorkerLease 构造函数要求令牌可取消,传默认值会直接抛 ArgumentException(fail-closed,不会静默降级为 forever-valid);确有「永不失去租约」的场景(如宿主静态分配 worker id)必须显式传入 SnowflakeWorkerLease.NeverLostToken,其余情况传真实的失锁通知 token。
  11. 同步释放容器:见「关闭时归还 lease」。
  12. 把逻辑时间戳当真实时间对账:超速时它会领先物理时钟;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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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