Sparkdo.DistributedLocking.Abstractions
3.0.0-preview.1
dotnet add package Sparkdo.DistributedLocking.Abstractions --version 3.0.0-preview.1
NuGet\Install-Package Sparkdo.DistributedLocking.Abstractions -Version 3.0.0-preview.1
<PackageReference Include="Sparkdo.DistributedLocking.Abstractions" Version="3.0.0-preview.1" />
<PackageVersion Include="Sparkdo.DistributedLocking.Abstractions" Version="3.0.0-preview.1" />
<PackageReference Include="Sparkdo.DistributedLocking.Abstractions" />
paket add Sparkdo.DistributedLocking.Abstractions --version 3.0.0-preview.1
#r "nuget: Sparkdo.DistributedLocking.Abstractions, 3.0.0-preview.1"
#:package Sparkdo.DistributedLocking.Abstractions@3.0.0-preview.1
#addin nuget:?package=Sparkdo.DistributedLocking.Abstractions&version=3.0.0-preview.1&prerelease
#tool nuget:?package=Sparkdo.DistributedLocking.Abstractions&version=3.0.0-preview.1&prerelease
Sparkdo.DistributedLocking.Abstractions
分布式锁的契约包:零依赖(不引用任何 Sparkdo 包、不引用第三方包、不注册任何服务),只定义「获取 → 持有 → 续租 → 释放」的语义边界与类型形状,供进程内 provider、Redis provider 与自研后端共同遵循。业务代码只依赖本包即可保持后端无关。
契约包不承诺任何互斥强度——互斥由 provider 实现保证,业务用 IDistributedLock.Provider 判定当前接线够不够用。
该引用哪个包
| 需要 | 引用 |
|---|---|
业务服务注入 IDistributedLock 做互斥,provider 由宿主接线 |
本包(一般由 provider 包传递引入,不必直接引用) |
| 单进程内多任务/多后台服务互斥,开箱可用 | Sparkdo.DistributedLocking |
| 多实例、多机器互斥 | Sparkdo.DistributedLocking.Redis |
| 自研后端(ZooKeeper、etcd、数据库 advisory lock 等) | 本包 + 实现 IDistributedLock |
只引用本包时容器里不会注册任何服务:IDistributedLock 无人实现,解析即失败。
契约地图
| 类型 | 形态 | 职责 |
|---|---|---|
IDistributedLock |
接口 | 获取入口(TryAcquireAsync 双重载)+ 后端身份声明(Provider) |
IDistributedLockHandle |
接口(IAsyncDisposable) |
一次持有权:Name、FencingToken、ExpiresAt、LostToken、IsLost、TryRenewAsync |
DistributedLockAcquisitionOptions |
sealed record(init-only) |
一次获取的等待/租约/自动续租配置 |
DistributedLockRenewalResult |
readonly record struct |
一次续租的结果:IsRenewed + 权威 ExpiresAt |
IDistributedLockKeyNormalizer |
接口 | 应用锁名 → provider 锁键的确定性纯映射 |
DistributedLockProviderInfo |
readonly record struct |
后端协调强度:Name + IsProcessLocal,含 InMemory / Redis 两个静态值 |
IDistributedLock:两种重载怎么选
| 场景 | 写法 | 拿不到时 |
|---|---|---|
| 拿不到就走别的分支(降级、跳过、换键) | await lk.TryAcquireAsync("orders:42") |
立即返回 null(默认 WaitTimeout = TimeSpan.Zero,单次尝试、不排队) |
| 最多等 5 秒 | await lk.TryAcquireAsync("orders:42", TimeSpan.FromSeconds(5)) |
超时耗尽后返回 null |
| 一直等到拿到 | await lk.TryAcquireAsync("orders:42", Timeout.InfiniteTimeSpan, cancellationToken) |
只在取消或拿到时结束 |
| 持有时间可能超过默认 30 秒 | TryAcquireAsync(name, new DistributedLockAcquisitionOptions { LeaseDuration = ... }) |
同上 |
| 持有时间不确定,需要后台续租 | options 重载 + AutoRenewalInterval(必须小于 LeaseDuration) |
同上 |
便捷重载只定等待,不定租约:TryAcquireAsync(name, timeout) 内部用默认租约 30 秒且不自动续租。超过 30 秒的工作若用这个重载,会在你继续写入时静默失锁——长任务必须改用 options 重载。
返回值与异常的分工:
null= 锁被占用且等待超时耗尽,这是正常分支,不是异常;OperationCanceledException= 等待期间cancellationToken触发;既有持有者不受任何影响(实测:取消等待后原持有者IsLost=false、锁仍在手);ArgumentException=name为null、空串或全空白;ArgumentNullException=options为null;ArgumentOutOfRangeException= 选项字段非法(见下)。
IDistributedLockHandle:一次持有权
状态机:持有中 →(DisposeAsync)已释放 或(租约到期 / 续租失败 / 后端报告失锁)已失效。两个终态都不可逆,句柄不会自动重新取得锁,也不会替你重新排队。
| 成员 | 语义 | 用法要点 |
|---|---|---|
Name |
获取时传入的应用锁名(未加前缀的原始名) | 日志与排障用它,物理键查不到它 |
FencingToken |
本次持有的单调递增标识,续租后保持不变 | 只是标识:拒绝过期持有者的写入必须由受保护资源自己校验(见「fencing token 用法」) |
ExpiresAt |
当前租约的权威到期时间(UTC),随续租移动 | Redis provider 取值于服务端时钟;失锁后不再有意义 |
LostToken |
失锁时被取消的 CancellationToken |
必须一路传进受保护工作的每个 await;这是唯一能打断后台失锁的通道 |
IsLost |
是否已进入「失锁」终态 | 正常释放不置位(实测已释放句柄 IsLost=false) |
TryRenewAsync(leaseDuration?, ct) |
延长租约 | 见下 |
句柄的四条使用契约(违反任何一条都会破坏互斥):
- 必须由成功获取的一方释放:
await using或显式await handle.DisposeAsync(); - 不得注册为 DI 单例——单例会永久持有一次持有权;
- 不得跨请求缓存句柄;
- 不得把同一句柄交给多个 owner(
DisposeAsync幂等只意味着重复释放安全,不意味着共享安全)。
三个失锁信号的分工(判断「现在还能不能继续写」用第一个):
| 信号 | 特点 |
|---|---|
LostToken.IsCancellationRequested |
唯一能被后台事件(定时器到期、自动续租失败)及时打断业务的通道 |
IsLost |
只表示「失锁」终态;正常释放后的句柄仍为 false,因此不能当作「能否继续写」的判据 |
TryRenewAsync 返回值 |
主动确认持有的唯一手段;在已正常释放的句柄上调用返回 IsRenewed=false 但不取消 LostToken、不抛异常 |
TryRenewAsync 参数与结果:
| 输入/输出 | 行为 |
|---|---|
leaseDuration = null(默认) |
沿用获取锁时的 LeaseDuration |
leaseDuration 非空且 <= TimeSpan.Zero |
抛 ArgumentOutOfRangeException |
cancellationToken 传入时已触发 |
抛 OperationCanceledException——检查发生在句柄状态判定之前,不会接触后端 |
| 续租成功 | Renewed(权威到期时间),FencingToken 不变 |
| 句柄已是「已释放」终态 | 返回 Lost(),且不改变任何状态(IsLost 仍 false、LostToken 不取消、不接触后端) |
| 本地判定租约已过 / 后端无法确认持有 | Lost()(fail-closed):句柄进入「已失效」终态并取消 LostToken |
Redis provider 的续租会在服务端 CAS 之前用注入时钟做一次本地预检:GetUtcNow() >= ExpiresAt 即直接判失锁、不发命令;因此本地时钟大幅快于服务端会提前失锁(把租约设长或信任 ExpiresAt 来自服务端这一事实)。
DistributedLockAcquisitionOptions
| 字段 | 默认 | 约束 | 非法后果 |
|---|---|---|---|
WaitTimeout |
TimeSpan.Zero(单次尝试、不排队) |
非负,或 Timeout.InfiniteTimeSpan |
其他负值抛 ArgumentOutOfRangeException |
LeaseDuration |
30 秒 | 必须大于零 | <= Zero 抛 ArgumentOutOfRangeException |
AutoRenewalInterval |
null(关闭自动续租,只允许手动续租) |
必须大于零且小于 LeaseDuration |
<= Zero 或 >= LeaseDuration 抛 ArgumentOutOfRangeException |
校验发生在任何后端动作之前:非法选项不会留下半截持锁。契约不设租约时长上限——超过定时器实现上限(约 49.7 天)的租约照样可用,provider 会截断定时器 dueTime 而不是抛异常;但 LeaseDuration 大到到期时间不可表示时(如 TimeSpan.MaxValue)仍然会抛 ArgumentOutOfRangeException,而这一步发生在持有权已经到手之后,所以 provider 必须先归还持有权再原样抛出(本域两个内置实现都这么做)。
DistributedLockAcquisitionOptions 是 init-only record:可以安全地作为常量对象在多次获取间复用(provider 只读不改)。
DistributedLockRenewalResult
| 构造 | IsRenewed |
ExpiresAt |
句柄后续 |
|---|---|---|---|
Renewed(expiresAt) |
true |
后端权威到期时间 | 继续持有,FencingToken 不变 |
Lost() |
false |
null |
句柄应进入「已失效」终态并取消 LostToken(已正常释放的句柄除外) |
provider 作者在「无法确认持有」时必须返回 Lost() 而不是抛通信异常——这是 fail-closed 的落点(宁可丢锁,不可双持)。
IDistributedLockKeyNormalizer
实现契约:确定性纯映射。相同输入在同一配置下必须得到相同键,结果不得随调用次数、时间、随机数或持有者身份变化。
public sealed class TenantScopedLockKeyNormalizer(LockContext context) : IDistributedLockKeyNormalizer
{
public string NormalizeKey(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
return $"tenant:{context.CurrentTenantId}:{name}"; // 同一持锁请求内必须稳定
}
}
映射本身可以读环境(租户、命名空间),但同一个 name 在同一配置/同一作用域内必须始终得到同一个键。禁止的做法:为键附加 nonce/时间戳(会让同名的两次获取落在不同键上,互斥直接失效)、返回与 name 无关的常量(所有锁互相阻塞)、抛非 ArgumentException 的异常。DefaultDistributedLockKeyNormalizer(在 Sparkdo.DistributedLocking 包)只做 KeyPrefix + name 拼接。
DistributedLockProviderInfo 与能力门禁
Provider 不是展示信息,是业务侧唯一的强度判据:
if (distributedLock.Provider.IsProcessLocal && !options.AllowProcessLocalWorkerLease)
{
throw new InvalidOperationException(
$"worker lease 需要跨进程协调:当前 provider '{distributedLock.Provider.Name}' 仅进程内互斥。");
}
真实消费方是 Sparkdo.IdGeneration.Snowflake 的 DistributedLockSnowflakeWorkerIdProvider——它会因进程内 provider 而拒绝启动,除非显式允许。
语义矩阵(provider 无关,两个内置实现一致,测试守卫)
| 场景 | 契约要求 |
|---|---|
| 锁被占用且超时耗尽 | 返回 null,不抛异常 |
| 等待中被取消 | 抛 OperationCanceledException;既有持有者不受影响 |
| 空白锁名 | 抛 ArgumentException(不是返回 null) |
| 选项非法 | 抛 ArgumentOutOfRangeException,且在接触后端之前 |
| 租约到期 | 句柄进终态:IsLost=true、LostToken 取消 |
| 续租无法确认持有 | fail-closed:返回 Lost(),不向业务抛通信异常 |
| 重复释放 | 幂等,不产生第二次后端动作 |
| fencing token | 同一键严格递增;跨进程语义由 provider 决定 |
| 重入 | 不支持:同键第二次获取只会排队或超时,同句柄也没有「再加一层」的 API |
自研 provider 的实现清单
Provider如实声明强度:只有真正跨进程协调才IsProcessLocal=false;- 获取失败一律
null,只有用户取消才OperationCanceledException; - 选项校验先于任何后端动作(不要留下无人认领的持锁);
- 句柄终态不可逆、
DisposeAsync幂等;失锁必须同时取消LostToken; - 无法确认持有时续租返回
Lost()(不抛); - 同键
FencingToken严格递增;不能保证时必须在包文档写明重置条件; ExpiresAt优先取后端权威时钟,不可得时用注入的TimeProvider;- 不做重入,不阻塞不同键;
- DI 注册用
TryAdd语义,让宿主可以先注册自己的实现覆盖; - 持有权已经到手之后的任何抛出路径(句柄构造、定时器编排、后端结果换算)都必须先归还持有权再原样抛出——绝不允许留下无人认领的锁;归还自身的异常不覆盖原始异常。
安装
dotnet add package Sparkdo.DistributedLocking.Abstractions
目标框架 net10.0;net11.0,Native AOT / trimming 兼容(契约包无反射、无运行时扫描)。通常不必直接引用——安装 Sparkdo.DistributedLocking 或 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
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Sparkdo.DistributedLocking.Abstractions:
| Package | Downloads |
|---|---|
|
Sparkdo.DistributedLocking
Sparkdo DistributedLocking 契约的进程内 provider 与 DI 组合。 |
|
|
Sparkdo.BackgroundJobs
Sparkdo 后台作业库,提供后台任务调度和执行功能实现 |
|
|
Sparkdo.VerificationCode
Sparkdo 验证码核心实现:CSPRNG 生成器、零分配校验路径的 PBKDF2 hash、cache/lock-backed manager、发送间隔限频、cache TTL 投影与 AddSparkdoVerificationCode 一行接线。 |
|
|
Sparkdo.IdGeneration.Snowflake
Sparkdo 的 Snowflake long Id 生成运行时:worker lease、逻辑时间状态与 DI 接线。 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-preview.1 | 87 | 10/30/2026 |
| 2.0.0-preview.2 | 105 | 7/20/2026 |
| 2.0.0-preview.1 | 106 | 7/18/2026 |
| 1.0.4-preview.3 | 240 | 6/8/2026 |
| 1.0.4-preview.2 | 224 | 6/7/2026 |
| 1.0.4-preview.1 | 229 | 6/7/2026 |
| 1.0.3 | 573 | 6/6/2026 |
| 1.0.3-preview.1 | 222 | 5/17/2026 |
| 1.0.2 | 408 | 4/27/2026 |
| 1.0.2-preview.5 | 127 | 4/15/2026 |
| 1.0.2-preview.4 | 143 | 2/8/2026 |
| 1.0.2-preview.3 | 144 | 2/1/2026 |
| 1.0.2-preview.2 | 148 | 1/31/2026 |
| 1.0.2-preview.1 | 229 | 12/4/2025 |
| 1.0.1 | 1,094 | 11/27/2025 |
| 1.0.0 | 1,048 | 11/25/2025 |
| 1.0.0-preview.5 | 180 | 10/24/2025 |