Sparkdo.DistributedLocking.Abstractions 3.0.0-preview.1

This is a prerelease version of Sparkdo.DistributedLocking.Abstractions.
dotnet add package Sparkdo.DistributedLocking.Abstractions --version 3.0.0-preview.1
                    
NuGet\Install-Package Sparkdo.DistributedLocking.Abstractions -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.DistributedLocking.Abstractions" 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.DistributedLocking.Abstractions" Version="3.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Sparkdo.DistributedLocking.Abstractions" />
                    
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.DistributedLocking.Abstractions --version 3.0.0-preview.1
                    
#r "nuget: Sparkdo.DistributedLocking.Abstractions, 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.DistributedLocking.Abstractions@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.DistributedLocking.Abstractions&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sparkdo.DistributedLocking.Abstractions&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Tool

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) 延长租约 见下

句柄的四条使用契约(违反任何一条都会破坏互斥):

  1. 必须由成功获取的一方释放:await using 或显式 await handle.DisposeAsync();
  2. 不得注册为 DI 单例——单例会永久持有一次持有权;
  3. 不得跨请求缓存句柄;
  4. 不得把同一句柄交给多个 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 的实现清单

  1. Provider 如实声明强度:只有真正跨进程协调才 IsProcessLocal=false;
  2. 获取失败一律 null,只有用户取消才 OperationCanceledException;
  3. 选项校验先于任何后端动作(不要留下无人认领的持锁);
  4. 句柄终态不可逆、DisposeAsync 幂等;失锁必须同时取消 LostToken;
  5. 无法确认持有时续租返回 Lost()(不抛);
  6. 同键 FencingToken 严格递增;不能保证时必须在包文档写明重置条件;
  7. ExpiresAt 优先取后端权威时钟,不可得时用注入的 TimeProvider;
  8. 不做重入,不阻塞不同键;
  9. DI 注册用 TryAdd 语义,让宿主可以先注册自己的实现覆盖;
  10. 持有权已经到手之后的任何抛出路径(句柄构造、定时器编排、后端结果换算)都必须先归还持有权再原样抛出——绝不允许留下无人认领的锁;归还自身的异常不覆盖原始异常。

安装

dotnet add package Sparkdo.DistributedLocking.Abstractions

目标框架 net10.0;net11.0,Native AOT / trimming 兼容(契约包无反射、无运行时扫描)。通常不必直接引用——安装 Sparkdo.DistributedLocking 或 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.
  • 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