ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

.NET 10 WebAPI 分布式锁实战:Redis+Lua+看门狗,解决并发超卖与重复提交

2026/9/8 13:30:02 拓冰建站 浏览量
.NET 10 WebAPI 分布式锁实战:Redis+Lua+看门狗,解决并发超卖与重复提交 .NET 10 还没正式发布的时候很多人已经在讨论“后端还有没有搞头”。我的看法是语言和框架更迭远没有并发场景里的“锁”问题过时。今天直接给你一套能落地的方案WebAPI Redis 分布式锁实现锁超时自动释放、看门狗续期、可重入、防误删锁。这套设计在电商库存扣减、定时任务防重复执行、同账号重复提交这些场景里非常实用。先看核心能力基于 .NET 10 的 WebAPI 项目使用 StackExchange.Redis 客户端通过 Lua 脚本保证加锁和解锁的原子性引入看门狗机制让锁在业务未完成时自动续期支持同一线程重入避免递归或嵌套业务死锁删除锁时校验客户端唯一标识防止误删其他请求的锁。代码可以直接复制到你的项目里改不需要额外引入 RedLock 等重量级组件。文章会从环境搭建开始带你创建 WebAPI 项目、配置 Redis、实现分布式锁核心类然后逐项验证超时释放、看门狗续期、可重入、防误删锁最后用并发请求压测一下实际效果。全程用 VS2022 或 Rider 都可以命令行的 dotnet CLI 也能跑。如果你正准备面试“分布式锁”相关岗位或者手上正有一个“前端点一次按钮后端收到多次提交”的 Bug 要修这篇文章建议先收藏。1. 核心能力速览能力项说明项目类型.NET 10 WebAPI 后端服务核心依赖StackExchange.Redis、Redis 服务端锁实现方式Redis SET NX EX Lua 脚本释放锁锁超时自动释放通过 Redis Key 过期时间实现看门狗续期后台 Timer 定期延长 Key 过期时间可重入锁同一线程可重复获取锁使用计数器维护防误删锁删除前校验客户端唯一标识 Value接口能力提供 HTTP API可被前端、定时任务、微服务调用批量任务支持多线程并发申请锁锁粒度到业务唯一 Key启动方式dotnet run / VS 直接运行适合场景库存扣减、订单防重、定时任务防重、缓存击穿保护这套实现不依赖第三方锁组件核心逻辑就两个类RedisLockManager和RedisLockHandle。前者管加锁、解锁、续期后者管锁的持有状态和 Dispose 释放。2. 适用场景与使用边界分布式锁不是所有并发问题的银弹你需要先分清哪些场景适合它。适合使用分布式锁的场景库存扣减多个用户同时抢购Redis 中的库存字段不能超卖。防重复提交前端同一订单在弱网下可能连点多次后端需要保证同一业务 Key 只处理一次。定时任务防重多实例部署的 Job 系统同一时刻只有一个实例执行某个任务。缓存击穿保护热点 Key 失效瞬间只允许一个请求回源数据库。不适合使用分布式锁的场景单机单进程内的简单并发用lock、SemaphoreSlim更轻量。强事务要求极高、需要数据库回滚协同的场景建议用数据库行锁或事务。需要严格分布式一致性如跨机房容灾时单点 Redis 锁有主从切换丢锁风险要考虑 RedLock 方案或 etcd 实现。安全边界也要提示一下所有锁的 Value 必须带上客户端唯一标识GUID防止 A 请求超时后锁被 B 请求获取A 业务结束后把 B 的锁删掉。这是生产事故最常见的来源。文中会把防误删写进核心代码。3. 环境准备与前置条件开始之前把你的环境过一遍。下面是明确的最低要求环境项要求操作系统Windows 10/11、Linux、macOS 均可.NET SDK.NET 10 Preview 或更高版本.NET 8 也可以编译部分 API 微调IDEVS2022 17.8 或 Rider或纯 CLIRedisRedis 6.xWindows 可用 Memurai 或 WSL/ Docker 运行包管理器NuGet磁盘空间2GB 以上SDK NuGet 缓存3.1 安装 .NET 10 SDK如果没有安装 SDK先到 .NET 官方下载页面拉取 SDK。安装完成后验证dotnet --version输出类似10.0.x就说明环境正常。3.2 准备 Redis 服务本机没有 Redis 时最简单的方式是用 Docker 起一个docker run -d --name redis-local \ -p 6379:6379 \ redis:7-alpineWindows 用户如果没装 Docker可以用 Memurai 或者 WSL 里装 Redis。启动后验证连接redis-cli -h 127.0.0.1 -p 6379 ping返回PONG表示 Redis 可用。密码、端口按你自己的环境改下面代码里的连接字符串也要注意替换。3.3 创建 WebAPI 项目dotnet new webapi -n RedisLockDemo cd RedisLockDemo dotnet add package StackExchange.Redis使用 VS2022 的读者直接新建 ASP.NET Core Web API 项目再通过 NuGet 安装 StackExchange.Redis 即可。默认建议勾选“使用控制器”省掉最小 API 的路由样板。4. 安装部署与启动方式先看一下项目的目录结构下面所有核心代码会逐步添加。RedisLockDemo/ ├── Controllers/ │ └── LockController.cs ├── Services/ │ ├── RedisLockManager.cs │ └── RedisLockHandle.cs ├── appsettings.json └── Program.cs4.1 配置 Redis 连接打开appsettings.json加入 Redis 配置{ Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: *, Redis: { ConnectionString: 127.0.0.1:6379,password,abortConnectfalse,connectTimeout5000, DefaultExpireSeconds: 30 } }abortConnectfalse很关键。它让客户端在 Redis 短暂不可用时不会直接抛异常而是进入重连等待状态。生产环境可以加syncTimeout5000调整同步超时。4.2 注册 Redis 服务在 Program.cs 中注册 StackExchange.Redis 的ConnectionMultiplexer单例再注册我们自己的锁管理器using RedisLockDemo.Services; using StackExchange.Redis; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); var redisConfig builder.Configuration.GetSection(Redis); var connectionString redisConfig[ConnectionString] ?? 127.0.0.1:6379; var defaultExpireSeconds int.Parse(redisConfig[DefaultExpireSeconds] ?? 30); builder.Services.AddSingletonIConnectionMultiplexer(sp { return ConnectionMultiplexer.Connect(connectionString); }); builder.Services.AddSingletonRedisLockManager(sp { var multiplexer sp.GetRequiredServiceIConnectionMultiplexer(); return new RedisLockManager(multiplexer, defaultExpireSeconds); }); var app builder.Build(); app.UseAuthorization(); app.MapControllers(); app.Run();这个注册方式保证整个进程内只维护一个 Redis 连接池避免每个请求都创建连接导致连接风暴。4.3 部署到 Kestrel开发环境直接跑dotnet run默认监听http://localhost:5xxx启动日志里会输出实际端口。想固定端口可以在 launchSettings.json 里改也可以运行时指定dotnet run --urls http://localhost:80805. 分布式锁核心代码实现这里开始进入正题。先写可释放的锁句柄再加锁管理器。所有 Redis 操作尽量使用 Lua 脚本保证原子性。5.1 锁句柄类RedisLockHandle一个锁句柄代表当前进程在某段时间内持有一把锁。它需要记录锁的 Key、Value、续期 Timer并在释放时做防误删删除。using StackExchange.Redis; namespace RedisLockDemo.Services; public sealed class RedisLockHandle : IDisposable { private readonly IDatabase _db; private readonly Timer? _watchdogTimer; private bool _disposed; public string LockKey { get; } public string LockValue { get; } public TimeSpan ExpireTime { get; private set; } public int ReentrantCount { get; set; } 1; public RedisLockHandle( IDatabase db, string lockKey, string lockValue, TimeSpan expireTime, TimeSpan watchdogInterval) { _db db; LockKey lockKey; LockValue lockValue; ExpireTime expireTime; _watchdogTimer new Timer( ExtendExpire, null, watchdogInterval, watchdogInterval); } private void ExtendExpire(object? state) { try { // 使用 Lua 脚本保证“值匹配则续期”是原子操作 var script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(pexpire, KEYS[1], ARGV[2]) else return 0 end ; var result _db.ScriptEvaluate(script, new RedisKey[] { LockKey }, new RedisValue[] { LockValue, ExpireTime.TotalMilliseconds }); var success (int)(long)result 1; if (!success) { // 锁已不存在或被其他线程持有停止续期 _watchdogTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } catch { // Redis 短暂异常时保留 Timer等待下一次续期 } } public void Dispose() { if (_disposed) return; _disposed true; _watchdogTimer?.Change(Timeout.Infinite, Timeout.Infinite); _watchdogTimer?.Dispose(); // 使用 Lua 脚本防误删 var script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end ; try { _db.ScriptEvaluate(script, new RedisKey[] { LockKey }, new RedisValue[] { LockValue }); } catch { // 释放失败时等待 Key 自然过期兜底 } } }这个类里最核心的是防误删逻辑。每次释放锁时Lua 脚本先比较 Value 是否当前客户端写入的唯一标识一致才删除。即使发生锁超时后已被其他线程持有当前线程释放时也只返回 0不会删除别人的锁。5.2 锁管理器RedisLockManager锁管理器负责对外提供加锁、释放、可重入判断三个能力。using StackExchange.Redis; namespace RedisLockDemo.Services; public sealed class RedisLockManager { private readonly IConnectionMultiplexer _multiplexer; private readonly TimeSpan _defaultExpireTime; private readonly TimeSpan _watchdogInterval; private readonly System.Collections.Concurrent.ConcurrentDictionarystring, RedisLockHandle _localLocks new(); public RedisLockManager(IConnectionMultiplexer multiplexer, int defaultExpireSeconds) { _multiplexer multiplexer; _defaultExpireTime TimeSpan.FromSeconds(defaultExpireSeconds); _watchdogInterval TimeSpan.FromSeconds(defaultExpireSeconds / 3.0); } /// summary /// 尝试获取分布式锁。 /// 成功返回锁句柄失败返回 null。 /// /summary public RedisLockHandle? TryAcquireLock(string lockKey, TimeSpan? expireTime null) { var db _multiplexer.GetDatabase(); var value Guid.NewGuid().ToString(N); var expiry expireTime ?? _defaultExpireTime; // 可重入同一线程已经持有同一 Key 的锁时直接增加计数 var currentThreadId Environment.CurrentManagedThreadId; var localKey BuildLocalKey(lockKey, currentThreadId); if (_localLocks.TryGetValue(localKey, out var existing)) { existing.ReentrantCount; return existing; } var script if redis.call(set, KEYS[1], ARGV[1], NX, PX, ARGV[2]) then return 1 else return 0 end ; var result db.ScriptEvaluate(script, new RedisKey[] { lockKey }, new RedisValue[] { value, expiry.TotalMilliseconds }); if ((int)(long)result 1) { var handle new RedisLockHandle( db, lockKey, value, expiry, _watchdogInterval); _localLocks[localKey] handle; return handle; } return null; } /// summary /// 释放锁。 /// /summary public void ReleaseLock(RedisLockHandle? handle) { if (handle null) return; var currentThreadId Environment.CurrentManagedThreadId; var localKey BuildLocalKey(handle.LockKey, currentThreadId); if (_localLocks.TryGetValue(localKey, out var localHandle)) { localHandle.ReentrantCount--; if (localHandle.ReentrantCount 0) { return; // 还有外层锁不真正释放 } _localLocks.TryRemove(localKey, out _); } handle.Dispose(); } private static string BuildLocalKey(string lockKey, int threadId) ${lockKey}:{threadId}; }这段代码用ConcurrentDictionary做进程内可重入计数同一 Key、同一线程再次加锁时不会发送新的 Redis 命令直接增加计数。最后一次性释放时通过 Lua 脚本做 Redis 端的删除。可重入锁要特别注意RedisLockHandle里的ReentrantCount字段是进程内计数不写入 Redis。如果业务代码里出现 Task 跨线程传递Environment.CurrentManagedThreadId会变可重入判断会失效。生产环境建议用 AsyncLocal 保存线程上下文或者直接限制锁的使用范围在同步方法内。6. 控制器接口与业务测试光有锁类还不够我们写几个真实业务接口来验证。一个模拟库存扣减一个模拟订单创建一个测试可重入。6.1 模拟库存扣减using Microsoft.AspNetCore.Mvc; using RedisLockDemo.Services; namespace RedisLockDemo.Controllers; [ApiController] [Route(api/lock)] public class LockController : ControllerBase { private readonly RedisLockManager _lockManager; private static int _stock 100; public LockController(RedisLockManager lockManager) { _lockManager lockManager; } [HttpPost(deduct-stock)] public IActionResult DeductStock([FromQuery] string productId, [FromQuery] int quantity 1) { if (string.IsNullOrWhiteSpace(productId)) { return BadRequest(new { Success false, Message productId 不能为空 }); } var lockKey $lock:stock:{productId}; using var handle _lockManager.TryAcquireLock(lockKey, TimeSpan.FromSeconds(10)); if (handle null) { return Conflict(new { Success false, Message 系统繁忙请稍后重试 }); } try { if (_stock quantity) { return BadRequest(new { Success false, Message 库存不足 }); } // 模拟耗时业务 Thread.Sleep(100); _stock - quantity; return Ok(new { Success true, Stock _stock, Message 扣减成功 }); } catch (Exception ex) { return StatusCode(500, new { Success false, Message ex.Message }); } } }这里加了Thread.Sleep(100)模拟真实业务耗时。真实项目中不要用静态 int 存库存这里只是为了演示锁的效果。生产环境库存应该放 Redis 或数据库扣减逻辑用原子操作再包一层锁保护。6.2 测试可重入锁可重入的一个典型场景是一个接口内部先加锁然后调用另一个也需要同一把锁的方法。比如订单创建流程里检查库存和锁定库存分别封装在两个方法里。[HttpPost(reentrant-test)] public IActionResult ReentrantTest() { var lockKey lock:order:create; using var outerLock _lockManager.TryAcquireLock(lockKey, TimeSpan.FromSeconds(30)); if (outerLock null) { return Conflict(new { Success false, Message 获取外层锁失败 }); } var innerLock _lockManager.TryAcquireLock(lockKey, TimeSpan.FromSeconds(30)); if (innerLock null) { return StatusCode(500, new { Success false, Message 可重入锁失败同一线程无法再次获取锁 }); } _lockManager.ReleaseLock(innerLock); _lockManager.ReleaseLock(outerLock); return Ok(new { Success true, Message 可重入锁验证成功同一线程连续获取同一把锁未阻塞 }); }注意这里没有用using释放内层锁因为内层锁和外层锁实际指向同一个RedisLockHandle实例。如果对内层锁也调用Dispose会直接删除 Redis Key外层锁就失去了保护。因此必须统一走ReleaseLock方法用计数器控制释放时机。6.3 锁超时自动释放验证锁超时自动释放是 Redis Key 过期机制天然提供的兜底能力。业务如果崩溃、进程被杀、网络分区锁最终一定会过期。写一个接口模拟锁超时后另一种请求能重新获取锁[HttpPost(timeout-test)] public async TaskIActionResult TimeoutTest() { var lockKey lock:timeout:test; var handle _lockManager.TryAcquireLock(lockKey, TimeSpan.FromSeconds(5)); if (handle null) { return Conflict(new { Success false, Message 锁被持有 }); } // 模拟持有锁的线程意外终止不做任何释放操作 return Ok(new { Success true, Message 已获取锁等待 5 秒后自动过期 }); }调用这个接口后立即再调用一次第二次会返回 409 Conflict。等待 5 秒后再次调用就能成功获取锁。这个测试验证的就是“锁超时自动释放”的核心兜底能力。7. 看门狗续期机制与测试看门狗是分布式锁生产级方案里的关键点。锁没有设置超时时间Redis Key 不会过期一旦持有锁的进程崩溃其他线程永远拿不到锁锁超时时间设置过短业务还没执行完锁就过期并发请求立刻涌入。看门狗解决了这个矛盾给锁设置一个初始过期时间通过后台 Timer 持续续期只要锁持有者还活着锁就不会过期。看门狗的工作流程加锁时设置一个较短的过期时间比如 30 秒。启动一个 Timer每隔过期时间的三分之一执行一次续期。续期时通过 Lua 脚本检查 Value 是否还是本客户端的标识。如果锁还在就重置过期时间为 30 秒。如果锁已被别人持有停止续期并结束 Timer。当前项目里的RedisLockHandle已经实现了这个逻辑。续期 Interval 默认是过期时间的三分之一也就是 30 秒过期时每 10 秒续一次。验证看门狗是否生效的方法给 Redis 里的锁 Key 设置一个较短的过期时间比如 5 秒然后观察 Key 的 TTL 是否持续重置。[HttpPost(watchdog-test)] public async TaskIActionResult WatchdogTest() { var lockKey lock:watchdog:test; using var handle _lockManager.TryAcquireLock(lockKey, TimeSpan.FromSeconds(5)); if (handle null) { return Conflict(new { Success false, Message 锁被持有 }); } // 持续观察锁 TTL var db _lockManager.GetDatabase(); for (int i 0; i 3; i) { var ttl db.KeyTimeToLive(lockKey); Console.WriteLine($第 {i 1} 次观察 TTL: {ttl?.TotalSeconds} 秒); await Task.Delay(2000); } return Ok(new { Success true, Message 看门狗续期验证完成观察服务端日志 }); }这段代码需要一个暴露GetDatabase()的方法可以在RedisLockManager里加一个public IDatabase GetDatabase() _multiplexer.GetDatabase();正常现象是初始 TTL 5 秒每 2 秒观察一次TTL 一直在 3~5 秒之间波动而不是倒数到 0。如果 TTL 一直在倒数说明看门狗没有生效优先检查 Timer 是否被 GC 回收或者在异常捕获中把 Timer 停了。需要提醒的是看门狗续期是基于“进程内持有锁”的前提。如果应用实例发生 GC 停顿或线程池饥饿超过续期间隔锁仍然会过期。这是任何续期方案都无法完全避免的只能在业务设计上接受这个极小概率窗口。8. 接口 API 与批量任务集成学会了基本接口下一步就是把锁服务集成到你的真实场景里。这里给出两种常见集成方式。8.1 HTTP 接口调用把锁的获取和释放封装成 REST 接口适合多语言微服务调用。获取锁的接口需要两个参数锁的 Key 和期望的过期时间。POST /api/lock/acquire?keyorder:123expireSeconds10curl -X POST http://localhost:5180/api/lock/acquire?keyorder:123expireSeconds10返回示例{ Success: true, Token: f0a2b1c3d4e5f6a7b8c9d0e1f2a3b4c5, ExpireSeconds: 10 }释放锁时带上 Token防误删校验由 Lua 脚本完成curl -X POST http://localhost:5180/api/lock/release \ -H Content-Type: application/json \ -d {\key\:\order:123\,\token\:\f0a2b1c3d4e5f6a7b8c9d0e1f2a3b4c5\}如果是跨语言调用需要把 Token 返回给调用方后续释放锁时原样传回。自己的 .NET 服务内调用直接使用RedisLockManager即可不需要走 HTTP 层。8.2 批量任务防重复执行批量任务场景里分布式锁最常见的用法是保证同一批数据不会同时被两个 Worker 消费。清点任务列表时每个任务生成唯一的锁 Key能拿到锁的 Worker 才执行任务拿不到的 Worker 直接跳过。public async Task ExecuteBatchAsync(IEnumerablestring taskIds) { foreach (var taskId in taskIds) { var lockKey $lock:task:{taskId}; using var handle _lockManager.TryAcquireLock(lockKey, TimeSpan.FromMinutes(5)); if (handle null) { continue; // 其他 Worker 正在处理 } try { await ProcessTaskAsync(taskId); } catch (Exception ex) { // 记录日志任务失败标记重试 } } }批量处理场景特别要注意锁粒度和 Redis 连接数量。如果任务数非常多不要每遍历一个 Task 就新建 Redis 连接直接用前面注册的单例ConnectionMultiplexer。锁 Key 的过期时间不要设置太短尤其是处理超大数据量的任务时尽量依赖看门狗自动续期。9. 资源占用与性能观察分布式锁不是纯粹免费的抽象它会给 Redis 和 .NET 进程带来额外开销。观察性能时主要看三个指标。9.1 Redis 内存与 Key 数量每个锁对应一个 Redis Key锁的 Value 大约 36 字节GUIDKey 名称根据业务决定。高并发场景下大量锁 Key 同时存在Redis 内存会上升。可以定期使用redis-cli --bigkeys扫描最长 Key排查是否有不及时释放的锁。redis-cli --bigkeys9.2 Redis 命令 QPS加锁一次需要一次EVAL释放锁一次需要一次EVAL看门狗续期每 10 秒一次。100 QPS 的锁请求Redis 的 QPS 大约 200 到 300。这个数字对单机 Redis 完全没有压力但如果业务里每个请求都加锁解锁建议统计锁的调用频率避免锁成为 Redis 的瓶颈。9.3 观察 Timers 数量与线程占用看门狗 Timer 会废弃掉锁句柄后停止。但如果业务中大量使用using var handle每次锁持有 30 秒且每 10 秒续期一次100 个并发请求会启动 100 个 Timer。虽然 .NET 的 Timer 成本很低但设计上最好复用同一个 Timer 做批量续期。更简单的优化方法是把看门狗间隔调大比如 30 秒过期时 15 秒续期一次减少续期频率。9.4 降低锁开销的实践锁粒度越小越好优先使用业务主键作为锁 Key而不是全局限流 Key。锁持有时间越短越好只在需要保护的关键代码段加锁不要整个请求都加锁。Redis 操作合并加锁和业务逻辑里的 Redis 操作走同一个ConnectionMultiplexer避免新连接开销。启用abortConnectfalseRedis 短暂故障时等待而不是立即抛出异常。大批量任务建议对锁 Key 做分片比如lock:task:{taskId % 100}控制并发度。10. 常见问题与排查方法实际部署和使用中容易踩的坑不少。直接整理成表格方便你对照排查。问题现象可能原因排查方式解决方案所有请求都拿不到锁Redis 连接断开或 Key 未过期redis-cli查看 TTLinfo clients查看连接数检查 Redis 服务状态重启服务锁刚获取就丢失过期时间设置过短业务没跑完查看业务耗时和 Redis TTL 变化开启看门狗续期或调大默认过期时间误删其他线程的锁释放锁时没有校验 Value检查解锁 Lua 脚本是否判断 Value 一致使用统一RedisLockHandle.Dispose释放锁可重入锁失效异步方法跨线程传递打印线程 ID检查是否跨 Task用 AsyncLocal 保存锁上下文看门狗没有续期Timer 被 GC 回收或异常抛出后未处理在续期方法里加日志观察日志输出保证RedisLockHandle不被 GC捕获异常后继续续期锁 Key 大量堆积在 Redis业务线程崩溃using未执行redis-cli --scan --pattern lock:*设置锁过期时间加看门狗续期Redis 主从切换后锁丢失主节点宕机从节点丢失未同步的数据info replication查看同步状态生产环境使用 RedLock 或多实例一致性方案并发扣减超卖锁获取成功但库存扣减不是原子操作查看扣减逻辑是否在锁保护范围内锁内使用 Redis 原子递减StringDecrement启动后连接超时Redis 配置了密码但连接串未带检查 ConnectionString 的 password 字段修改连接字符串重启服务HTTP 接口返回 409锁已被其他请求持有查看 Redis TTL等锁过期重试或调整业务并发策略11. 最佳实践与使用建议到这里一个完整可用的 .NET 10 WebAPI Redis 分布式锁方案已经落地了。最后给几条工程化建议这些都是在真实项目里积累出来的经验。第一次接入先做最小验证。不要直接把锁套在核心业务上先用一个简单的测试接口跑通加锁、解锁、超时释放三个流程确认 Redis 连接和各参数符合预期再扩展到真实业务。锁内代码要短、要快。分布式锁保护的代码段应该尽可能只包含必须串行的部分。如果锁内涉及外部 HTTP 调用或数据库事务要明确设置超时时间避免业务线程卡死导致锁长期被持有。释放锁必须走 finally 或 using。业务代码里无论如何都不能跳过锁释放。using var handle是最保险的写法编译器会保证异常时也调用 Dispose。签名里面的返回值、异常捕获是否完整要做 Code Review。不要用固定字符串作为锁 Value。每个线程加锁必须使用独立的 GUID 或组合标识这是防误删的基础。锁 Key 要规范。统一用lock:业务域:业务ID的格式例如lock:stock:sku_1001。这样 Redis 里对锁 Key 的扫描、排查、按前缀清理都很方便。生产环境做监控。记录每次加锁失败的情况统计锁等待时间和持有时间。锁冲突率突然升高往往说明业务并发出现问题或锁粒度设计不合理。合规提醒。如果这个接口被前端直接调用要考虑用户身份认证和接口幂等防止恶意循环请求打爆 Redis。锁内处理的数据涉及用户隐私或订单信息日志不要全量打印尽量脱敏。12. 总结与下一步这套方案的核心价值有三个第一基于 Redis 原生 Lua 脚本实现加锁和解锁的原子性不引入额外第三方组件代码可控第二看门狗续期机制让锁不会因为业务超时而提前失效也不会因为进程崩溃而永久占锁第三可重入和防误删补上了容易被忽略的生产级细节让方案能真正用在订单、库存、定时任务这些关键链路上。如果你在 .NET 项目里还没接触过分布式锁建议先跑通库存扣减和可重入测试两个接口感受一下锁的获取和释放时机。已经用过 Redisson 这类库的读者可以对照本文的看门狗和 Lua 脚本实现看看自己项目里的锁释放是否也有 Value 校验是否有完善的续期机制。下一步可以继续扩展的方向用AsyncLocal改造可重入锁以支持异步链路集成 RedLock 方案解决 Redis 主从切换场景下的极端一致性把分布式锁封装成自定义 Attribute用 AOP 方式声明式加锁或者结合 .NET 10 的中间件体系把锁的获取和释放做成请求级自动管理。建议先把本文代码落到你的测试项目里跑一遍收藏备用遇到并发问题时回来翻一翻这张排查表。