Sparkdo.DistributedLocking 3.0.0-preview.1

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

Sparkdo.DistributedLocking

Sparkdo.DistributedLocking 是分布式锁契约的进程内 provider 与 DI 组合包:每键一个信号量排队、可注入 TimeProvider 的租约与自动续租、默认键规范化器,以及 AddSparkdoDistributedLocking() 一行接线。它传递引入 Sparkdo.DistributedLocking.Abstractions。

单机多任务互斥开箱可用;跨进程/跨机器互斥请改用 Sparkdo.DistributedLocking.Redis(本包 Provider.IsProcessLocal == true,业务侧可据此拒绝)。

快速开始

using Microsoft.Extensions.DependencyInjection;
using Sparkdo.DistributedLocking;

var services = new ServiceCollection();
services.AddSparkdoDistributedLocking(options => options.KeyPrefix = "tenant-a:");
var provider = services.BuildServiceProvider();

var distributedLock = provider.GetRequiredService<IDistributedLock>();

await using var handle = await distributedLock.TryAcquireAsync(
    "orders:42",
    new DistributedLockAcquisitionOptions
    {
        WaitTimeout = TimeSpan.FromSeconds(5),       // 最多等 5 秒,等不到返回 null
        LeaseDuration = TimeSpan.FromMinutes(1),     // 租约 1 分钟
        AutoRenewalInterval = TimeSpan.FromSeconds(20), // 每 20 秒自动续租(必须 < 租约)
    });

if (handle is null)
{
    return;   // 锁被占用:降级、跳过或换键,不是异常
}

// 持锁执行。租约到期或续租失败时 LostToken 被取消,受保护的工作必须据此停止。
await DoWorkAsync(handle.LostToken);
if (handle.LostToken.IsCancellationRequested)
{
    // 已经失锁:本次结果不可信,按业务规则重试或放弃
}

不走 DI 时可直接构造(与容器内注册的实例同型):

var normalizer = new DefaultDistributedLockKeyNormalizer(
    Microsoft.Extensions.Options.Options.Create(new DistributedLockingOptions { KeyPrefix = "tenant-a:" }));

IDistributedLock distributedLock = new InMemoryDistributedLock(normalizer);              // 系统时钟
IDistributedLock testable = new InMemoryDistributedLock(normalizer, myFakeTimeProvider); // 注入时钟

AddSparkdoDistributedLocking 注册了什么

services.AddSparkdoDistributedLocking();                    // 纯接线
services.AddSparkdoDistributedLocking(options => ...);      // 接线 + 追加默认 named options 配置
服务 默认实现 注册语义
DistributedLockingOptions AddOptions<DistributedLockingOptions>() 配置委托叠加,按注册顺序依次应用,同一字段后注册者胜出
TimeProvider TimeProvider.System TryAdd singleton,宿主先注册的时钟实现优先
IDistributedLockKeyNormalizer DefaultDistributedLockKeyNormalizer TryAdd singleton,宿主先注册的规范化器优先
IDistributedLock InMemoryDistributedLock(工厂,注入上面两项) TryAdd singleton,宿主先注册的 IDistributedLock 优先

因为全部是 TryAdd:重复调用 AddSparkdoDistributedLocking() 幂等(容器里始终只有一个 IDistributedLock),宿主自己注册的实现永不被覆盖。要换成 Redis provider 用的不是「后注册覆盖」,而是 AddSparkdoRedisDistributedLock() 的显式 RemoveAll。

InMemoryDistributedLock 单实例服务全部锁键,必须是 singleton;把它注册成 scoped/transient 会让每个作用域各持一套信号量,互斥立刻失效。

进程内 provider 的逐项行为

方面 行为
互斥单位 规范化锁键(KeyPrefix + name),每键一个 SemaphoreSlim(1, 1);不同键永不相互阻塞
单次尝试 WaitTimeout = TimeSpan.Zero(默认):立即返回成功或 null,不进入等待队列
限时/无限等待 正数时长或 Timeout.InfiniteTimeSpan 进入信号量等待队列,释放后由等待者接管;不是公平锁,也不承诺唤醒顺序
等待中取消 OperationCanceledException,等待者的租约不生效,既有持有者完全不受影响
租约 由注入的 TimeProvider.CreateTimer 调度;到期即进入「已失效」终态:IsLost=true、LostToken 取消、让出信号量供他人获取
自动续租 AutoRenewalInterval 到点把到期时间推进一个 LeaseDuration,FencingToken 不变;纯本地计时,没有任何后端确认(这正是它不能跨进程的原因)
手动续租 handle.TryRenewAsync(leaseDuration?);句柄已终态或本地已过期时返回 Lost(),不抛
fencing token 实例内 Interlocked 递增:同键严格递增,但与其他键共用计数器所以会跳号;进程重启从 1 重新开始,只在本进程内有意义
释放 DisposeAsync 幂等、总是成功、不抛异常(无网络);重复调用不会二次让出槽位
键的内存占用 条目带引用计数,归零即从字典移除,常驻内存不随用过的键数增长;信号量刻意不 Dispose——等待者可能正阻塞在 WaitAsync 上,提前释放会把它们变成 ObjectDisposedException,条目随字典移除整体交给 GC
超长租约 LeaseDuration/AutoRenewalInterval 超过定时器可实现上限(4294967294 ms ≈ 49.7 天)时,dueTime 被截断到上限,到期判定按 ExpiresAt 重排;获取不会抛,也不会留下无人认领的持锁
句柄构造失败 已经拿到信号量之后才构造/启动句柄,所以这一步的任何抛出(例如 LeaseDuration = TimeSpan.MaxValue 让到期时间溢出而抛 ArgumentOutOfRangeException)都会先归还持有权再原样抛出——同键不会因此永久无人认领

用法要点

忘记释放的两种后果,取决于是否设置 AutoRenewalInterval:

配置 泄漏句柄的行为 结论
不自动续租(默认) 租约到点自动让出槽位,最坏阻塞一个 LeaseDuration 靠租约兜底即可
设置自动续租 句柄永久续租,同键在本进程剩余生命周期内再也拿不到锁 必须 await using,或给持锁工作本身加超时

前缀只作用于键,不作用于名字:配了 KeyPrefix = "tenant-a:" 后,TryAcquireAsync("orders") 与 TryAcquireAsync("tenant-a:orders") 落在两个不同键上,互相不阻塞(实测两者都能拿到);handle.Name 始终是传入的原始名。多租户/多应用共用一套 provider 时,前缀是唯一隔离入口,但同一进程内的前缀只能有一份(默认 IDistributedLockKeyNormalizer 只读 DistributedLockingOptions.KeyPrefix)。

测试可直接推进虚拟时钟,不需要真实等待:

var clock = new FakeTimeProvider(DateTimeOffset.UnixEpoch);      // 任何 TimeProvider 实现
var distributedLock = new InMemoryDistributedLock(normalizer, clock);
var handle = await distributedLock.TryAcquireAsync("orders",
    new DistributedLockAcquisitionOptions { LeaseDuration = TimeSpan.FromSeconds(10) });

clock.Advance(TimeSpan.FromSeconds(10));

handle!.IsLost.ShouldBeTrue();                                    // 到期即失锁
var next = await distributedLock.TryAcquireAsync("orders");       // 非 null:槽位已让出

同一个 TimeProvider 也决定了 TryRenewAsync 的本地预检,所以「到期后续租报 Lost」这类用例完全确定性。

选项

选项 默认 说明
DistributedLockingOptions.KeyPrefix string.Empty(不加前缀) 追加在每个规范化锁键之前;多租户或多应用共用后端时用它隔离同名锁。空白锁名在规范化处抛 ArgumentException

KeyPrefix 在 DefaultDistributedLockKeyNormalizer 构造时读取一次并冻结;宿主用 IOptionsMonitor 热更新前缀不会影响已注册的 singleton provider——需要动态前缀请实现自己的 IDistributedLockKeyNormalizer。

替换与扩展

想做的事 做法
升级为跨进程锁 services.AddSparkdoRedisDistributedLock()(它对 IDistributedLock 先 RemoveAll 再注册,显式覆盖本包默认)
用自研/第三方锁 在调用 AddSparkdoDistributedLocking() 之前注册 IDistributedLock 实现(TryAdd 不会覆盖)
改键布局 先注册自定义 IDistributedLockKeyNormalizer(须是确定性纯映射)
测租约/续租 先注册自定义 TimeProvider(虚拟时钟),两个 provider 都吃它
不用容器 new InMemoryDistributedLock(normalizer[, timeProvider])
同时要进程内和跨进程两把锁体系 一个走 AddSparkdoRedisDistributedLock(),另一个手工注册为具体类型并在业务侧按类型注入(IDistributedLock 单服务只解析出一个)

边界与非目标

  • 只在当前进程内互斥:Provider.IsProcessLocal == true,多实例部署必须换 Redis provider;
  • 进程退出即丢失全部持锁状态,没有持久化、没有恢复、没有崩溃后的租约接管;
  • 不可重入:同键第二次获取只会排队或超时,句柄上也没有「再加一层」的 API;
  • 不区分持有者身份:同进程任何代码路径都能在持锁者释放后拿到锁,不提供线程/请求归属校验;
  • 不保证等待公平性与唤醒顺序,也不提供优先级继承;
  • ExpiresAt 与续租全部来自本地时钟,没有服务端权威。

异常一览

异常 触发点
ArgumentException 锁名为 null/空/全空白(获取入口与规范化器都拦截)
ArgumentNullException options 为 null;构造参数 keyNormalizer/timeProvider 为 null
ArgumentOutOfRangeException WaitTimeout 为除 Timeout.InfiniteTimeSpan 外的负值;LeaseDuration <= 0;AutoRenewalInterval <= 0 或 >= LeaseDuration;TryRenewAsync 的 leaseDuration <= 0;LeaseDuration 大到 UtcNow + LeaseDuration 溢出(这一种发生在已拿到锁之后,provider 会先归还持有权再抛)
OperationCanceledException 等待中被取消;TryRenewAsync 传入已触发的 token

获取失败(锁被占用)返回 null,不是异常。

安装

dotnet add package Sparkdo.DistributedLocking

目标框架 net10.0;net11.0,IsAotCompatible + trimming 兼容;包内无运行时扫描、无反射序列化。

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 (2)

Showing the top 2 NuGet packages that depend on Sparkdo.DistributedLocking:

Package Downloads
Sparkdo.DistributedLocking.StackExchangeRedis

Sparkdo 分布式锁 Redis 实现库,基于 StackExchange.Redis 和 Medallion.Threading.Redis 提供分布式锁功能的 Redis 实现

Sparkdo.DistributedLocking.Redis

基于 Redis 单实例原子命令的跨进程分布式锁 provider。

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-preview.1 63 10/30/2026
2.0.0-preview.2 104 7/20/2026
2.0.0-preview.1 92 7/18/2026
1.0.4-preview.3 93 6/8/2026
1.0.4-preview.2 89 6/7/2026
1.0.4-preview.1 90 6/7/2026
1.0.3 197 6/6/2026
1.0.3-preview.1 87 5/17/2026
1.0.2 159 4/27/2026
1.0.2-preview.5 90 4/15/2026
1.0.2-preview.4 121 2/8/2026
1.0.2-preview.3 108 2/1/2026
1.0.2-preview.2 103 1/31/2026
1.0.2-preview.1 220 12/4/2025
1.0.1 299 11/27/2025
1.0.0 299 11/25/2025
1.0.0-preview.5 175 10/24/2025