.NET 10 WebAPI 分布式锁实战:Redis+Lua+看门狗,解决并发超卖与重复提交
2026/9/8 13:29:57 网站建设 项目流程

.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 直接运行
适合场景库存扣减、订单防重、定时任务防重、缓存击穿保护

这套实现不依赖第三方锁组件,核心逻辑就两个类:RedisLockManagerRedisLockHandle。前者管加锁、解锁、续期,后者管锁的持有状态和 Dispose 释放。

2. 适用场景与使用边界

分布式锁不是所有并发问题的银弹,你需要先分清哪些场景适合它。

适合使用分布式锁的场景:

  • 库存扣减:多个用户同时抢购,Redis 中的库存字段不能超卖。
  • 防重复提交:前端同一订单在弱网下可能连点多次,后端需要保证同一业务 Key 只处理一次。
  • 定时任务防重:多实例部署的 Job 系统,同一时刻只有一个实例执行某个任务。
  • 缓存击穿保护:热点 Key 失效瞬间,只允许一个请求回源数据库。

不适合使用分布式锁的场景:

  • 单机单进程内的简单并发,用lockSemaphoreSlim更轻量。
  • 强事务要求极高、需要数据库回滚协同的场景,建议用数据库行锁或事务。
  • 需要严格分布式一致性(如跨机房容灾)时,单点 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,或纯 CLI
RedisRedis 6.x+,Windows 可用 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-alpine

Windows 用户如果没装 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.cs

4.1 配置 Redis 连接

打开appsettings.json,加入 Redis 配置:

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "Redis": { "ConnectionString": "127.0.0.1:6379,password=,abortConnect=false,connectTimeout=5000", "DefaultExpireSeconds": 30 } }

abortConnect=false很关键。它让客户端在 Redis 短暂不可用时不会直接抛异常,而是进入重连等待状态。生产环境可以加syncTimeout=5000调整同步超时。

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.AddSingleton<IConnectionMultiplexer>(sp => { return ConnectionMultiplexer.Connect(connectionString); }); builder.Services.AddSingleton<RedisLockManager>(sp => { var multiplexer = sp.GetRequiredService<IConnectionMultiplexer>(); 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:8080

5. 分布式锁核心代码实现

这里开始进入正题。先写可释放的锁句柄,再加锁管理器。所有 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.ConcurrentDictionary<string, 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 Task<IActionResult> 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 持续续期,只要锁持有者还活着,锁就不会过期。

看门狗的工作流程:

  1. 加锁时设置一个较短的过期时间(比如 30 秒)。
  2. 启动一个 Timer,每隔过期时间的三分之一执行一次续期。
  3. 续期时,通过 Lua 脚本检查 Value 是否还是本客户端的标识。
  4. 如果锁还在,就重置过期时间为 30 秒。
  5. 如果锁已被别人持有,停止续期并结束 Timer。

当前项目里的RedisLockHandle已经实现了这个逻辑。续期 Interval 默认是过期时间的三分之一,也就是 30 秒过期时,每 10 秒续一次。

验证看门狗是否生效的方法:

给 Redis 里的锁 Key 设置一个较短的过期时间,比如 5 秒,然后观察 Key 的 TTL 是否持续重置。

[HttpPost("watchdog-test")] public async Task<IActionResult> 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?key=order:123&expireSeconds=10
curl -X POST "http://localhost:5180/api/lock/acquire?key=order:123&expireSeconds=10"

返回示例:

{ "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(IEnumerable<string> 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 字节(GUID),Key 名称根据业务决定。高并发场景下大量锁 Key 同时存在,Redis 内存会上升。可以定期使用redis-cli --bigkeys扫描最长 Key,排查是否有不及时释放的锁。

redis-cli --bigkeys

9.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,避免新连接开销。
  • 启用abortConnect=false,Redis 短暂故障时等待而不是立即抛出异常。
  • 大批量任务建议对锁 Key 做分片,比如lock:task:{taskId % 100}控制并发度。

10. 常见问题与排查方法

实际部署和使用中容易踩的坑不少。直接整理成表格,方便你对照排查。

问题现象可能原因排查方式解决方案
所有请求都拿不到锁Redis 连接断开或 Key 未过期redis-cli查看 TTL,info 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 的中间件体系,把锁的获取和释放做成请求级自动管理。建议先把本文代码落到你的测试项目里跑一遍,收藏备用,遇到并发问题时回来翻一翻这张排查表。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询