.NET 10 WebAPI 生产级 Redis 分布式锁实战:从原理到代码 面对高频并发、库存扣减、重复提交这类场景分布式锁几乎是每个后端开发都绕不开的硬话题。很多同学在本地写代码时一切正常一到项目上线就发现lock锁不住别服务器的线程Monitor也只在自己进程里有效。最近 .NET 10 的讨论热度上来了AI 辅助编程工具也确实能一键生成“加锁代码”但锁超时怎么自动释放、业务没跑完锁先没了怎么办、可重入怎么实现、Redis 锁会不会误删别人的锁……这些问题光靠 AI 生成代码是解决不了的。这篇文章围绕 .NET 10 / .NET 8 WebAPI 项目从原理到代码完整拆解一个生产级 Redis 分布式锁方案。我们会实现锁超时自动释放、看门狗续期、可重入锁、防误删锁四个核心能力并提供可直接运行的示例代码。无论你是准备面试还是要在真实项目中落地都可以按这套思路往下走。1. 为什么需要分布式锁1.1 从一次并发扣库存的“事故”说起假设你有一个下单接口业务逻辑很简单查询库存、判断是否充足、扣减库存。单机部署时可以用lock或Monitor把这段逻辑串行化private readonly object _stockLock new object(); public void DecreaseStock(string productId, int count) { lock (_stockLock) { // 1. 查询库存 // 2. 判断库存是否充足 // 3. 扣减库存 } }单机版一切正常。但一旦服务部署成多个实例前面加 Nginx 负载均衡问题就来了两个请求被分发到不同的进程进程 A 和进程 B 各自持有自己的_stockLock谁也拦不住谁。最终可能出现库存从 10 直接被扣到 -2 的情况也就是我们常说的“超卖”。这时我们需要一把“跨进程的锁”。所有服务实例都去同一个第三方协调者那里申请锁谁拿到锁谁才能执行这段业务代码。这个协调者可以是数据库、ZooKeeper、Etcd也可以是我们很熟悉的 Redis。1.2 分布式锁的典型应用场景分布式锁最常见的场景包括以下几类库存扣减、订单创建等需要“判断 写入”复合操作的场景秒杀、抢购、优惠券领取等瞬时高并发场景定时任务在分布式部署环境下的重复执行控制防重复提交例如防止用户连续点击提交按钮导致重复下单分布式事务中的幂等控制保证同一请求只被处理一次。这些场景有一个共同特点它们不只是一个简单的 “Redis INCR” 就能解决的原子操作而是涉及到先查询、再判断、最后写入的完整业务流程。锁的作用是把这个业务流程在逻辑上执行串行化。1.3 Redis 分布式锁 vs 其它方案常见分布式锁实现有数据库锁、ZooKeeper 临时顺序节点锁、Redis 分布式锁三种思路。数据库锁实现简单性能瓶颈明显而且数据库本身容易出现连接池打满的问题ZooKeeper 锁可靠性强但引入额外组件运维成本高Redis 锁性能高、实现简单、接入成本低是目前互联网项目中使用最广泛的一种方案。Redis 分布式锁也并非没有缺点比如主从切换时可能出现锁丢失这就是后面要提到的 RedLock 讨论的出发点。但从工程角度来说Redis 分布式锁足够满足绝大多数业务场景尤其是在锁超时、看门狗续期这些细节都实现到位的情况下稳定性会大幅提升。2. 环境准备与版本说明2.1 开发环境在开始写代码之前先把环境说明清楚。本文示例以 Windows .NET 10 为背景但代码层面基本是 .NET 8 通用语法。如果你目前还在用 .NET 8 或 .NET 9只需要把目标框架改成对应的版本即可。操作系统Windows 10/11 或 Linux 均可IDEVisual Studio 202217.10、JetBrains Rider 或 VS CodeSDK.NET 8 / .NET 9 / .NET 10 SDKRedis5.0 以上版本推荐 7.xNuGet 包StackExchange.Redis请以官方最新稳定版本为准。关于 .NET 10 需要特别提醒一句如果你在正式生产环境使用 .NET 10建议先确认内部验证通过或使用官方 LTS 版本。本文的核心代码不依赖 .NET 10 独有的新特性所以完全可以在 .NET 8 项目里复用。2.2 Redis 安装与可视化工具本地开发推荐使用 Docker 快速启动 Redisdocker run --name redis7 -p 6379:6379 -d redis:7.4如果不想用 Docker也可以直接在官网下载 Redis for Windows 的安装包或者使用 Memurai 这类兼容 Redis 协议的 Windows 服务。安装完成后用 Redis Desktop Manager 或 Another Redis Desktop Manager 这类可视化客户端连接 Redis后面观察锁的 key、TTL、值都会方便很多。2.3 初始化 .NET WebAPI 项目先创建项目dotnet new webapi -n OrderApi cd OrderApi dotnet add package StackExchange.Redis创建完成后项目结构如下OrderApi/ ├── OrderApi.csproj ├── Program.cs ├── appsettings.json ├── Controllers/ │ └── OrdersController.cs ├── Infra/ │ ├── RedisOptions.cs │ ├── RedisService.cs │ ├── RedisDistributedLock.cs │ └── LockLease.cs └── Services/ └── OrderService.cs下面我们从原理入手先把 Redis 分布式锁的关键知识点讲透再动手写完整代码。3. Redis 分布式锁核心原理3.1 加锁SET NX EXRedis 分布式锁的基础命令是SET key value NX EX。先看命令格式SET lockKey lockToken NX EX 30其中lockKey锁的名称例如order:lock:p-1001lockToken锁的唯一标识通常是一个 UUID 或 GUID 字符串NX只有当 key 不存在时才能设置成功这保证了“互斥”EX 30设置过期时间为 30 秒防止客户端宕机后锁永远不释放。这个命令最大的价值是原子性。SET NX EX一步完成“判断存在 写入 设置过期时间”不会出现SETNX和EXPIRE分两步执行时进程崩溃导致锁永久不释放的问题。在 StackExchange.Redis 中对应的方法是bool isLocked await db.StringSetAsync(lockKey, lockToken, expiry, When.NotExists);When.NotExists就对应命令中的NX参数expiry对应EX参数。3.2 解锁Lua 脚本与防误删解锁同样不是简单的DEL就能解决的。考虑一个场景请求 A 拿到锁业务执行时间较长锁超时自动释放了请求 B 随后拿到锁开始执行业务此时请求 A 如果直接执行DEL lockKey就会把请求 B 的锁删掉这属于典型的“误删别人锁”。正确的解锁流程是先比较锁的值是不是自己当初写入的 token如果是才允许删除。比较和删除这两个动作必须保证原子性所以要用 Lua 脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段脚本的意思是只有 key 中保存的值等于当前客户端持有的 token 时才执行删除否则直接返回 0。这个判断过程在 Redis 服务端执行不存在并发判断中间插入其他命令的窗口期。3.3 锁超时与看门狗续期加锁时设置过期时间解决了客户端宕机导致死锁的问题。但过期时间本身也带来了新的矛盾。如果过期时间设置太短比如 3 秒而业务执行需要 5 秒那么业务还没跑完锁就自动释放了其他线程就可能提前进入出现并发问题。如果过期时间设置太长比如 30 秒而业务只需要 200 毫秒那么一旦持有锁的线程真的崩溃其他线程最多需要等 30 秒才能拿到锁系统可用性会明显下降。生产级方案通常采用“看门狗续期”机制。核心思想是设置一个相对保守的锁过期时间比如 20 秒或 30 秒拿到锁之后后台起一个定时任务每隔过期时间 / 3就执行一次续期逻辑。只要业务还在执行、锁还在自己手上就不断把过期时间刷新回初始值。这样既避免了锁超时被业务拖垮的问题也保证了客户端宕机时锁能在最多一个过期周期后自动释放。3.4 可重入锁可重入的意思是同一个请求线程或异步上下文在已经持有锁的情况下再次申请同一把锁时不应该被阻塞也不应该重新等待而是直接成功同时锁的持有次数递增。为什么需要可重入因为在一个请求链路中外层的下单方法加了锁内部又调用了另一个同样需要加锁的服务方法如果锁不可重入第二次获取锁会失败甚至出现死锁等待。在 Redis 层面真正的可重入需要引入“锁计数器”。比较常见的做法是把锁的值设计成一个 Hash里面保存 token 和重入次数或者像本文一样在客户端内存里用 AsyncLocal 保存当前请求上下文已经持有的锁信息避免重复申请 Redis。3.5 RedLock 需要吗RedLock 是 Redis 官方推荐的分布式锁加强版方案核心思路是同时向 N 个独立的 Redis 节点申请锁只有大多数节点都加锁成功才认为获取锁成功从而应对单节点故障或主从切换带来的锁丢失问题。RedLock 能提升锁的可靠性但也让系统复杂度明显上升。实际项目中绝大多数业务对锁的容忍度并没有那么低即使发生极端的主从切换最多出现短暂的重入后续还可以用数据库乐观锁、库存扣减的 SQL 条件更新做兜底。所以本文不展开 RedLock 的代码实现而是在最佳实践部分给出取舍建议。4. 完整实战.NET 10 WebAPI Redis 分布式锁4.1 项目结构与依赖在OrderApi.csproj中确保已经引入 StackExchange.RedisProject SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet10.0/TargetFramework Nullableenable/Nullable ImplicitUsingsenable/ImplicitUsings /PropertyGroup ItemGroup PackageReference IncludeStackExchange.Redis Version2.8.16 / /ItemGroup /Project如果你使用 .NET 8将TargetFramework改为net8.0即可其余代码完全一致。4.2 配置 Redis 连接在appsettings.json中增加 Redis 配置{ Redis: { ConnectionString: localhost:6379,abortConnectfalse,defaultDatabase0, InstanceName: order-api, LockExpirySeconds: 30 }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }然后定义配置模型Infra/RedisOptions.csnamespace OrderApi.Infra; public class RedisOptions { public string ConnectionString { get; set; } string.Empty; public string InstanceName { get; set; } string.Empty; public int LockExpirySeconds { get; set; } 30; }接着封装 Redis 连接服务Infra/RedisService.csusing StackExchange.Redis; namespace OrderApi.Infra; public sealed class RedisService : IDisposable { private readonly ConnectionMultiplexer _connection; public RedisService(string connectionString) { _connection ConnectionMultiplexer.Connect(connectionString); } public IDatabase Database _connection.GetDatabase(); public void Dispose() { _connection.Dispose(); } }这里使用ConnectionMultiplexer单例复用连接这是 StackExchange.Redis 官方推荐的做法避免每次操作都创建新连接导致连接池膨胀。4.3 使用 Redis 的字符串 SET 实现加锁接下来是核心的锁服务。先看锁的持有对象Infra/LockLease.cs它承担了三个职责保存锁的 key 和 token、启动看门狗续期、释放锁时使用 Lua 脚本安全删除。using StackExchange.Redis; namespace OrderApi.Infra; public sealed class LockLease : IAsyncDisposable { private readonly IDatabase _db; private readonly string _key; private readonly string _token; private readonly TimeSpan _expiry; private readonly CancellationTokenSource _cts new(); private readonly Task _renewalTask; private readonly RedisDistributedLock _owner; private int _depth; public string Key _key; public string Token _token; internal LockLease(IDatabase db, string key, string token, TimeSpan expiry, RedisDistributedLock owner) { _db db; _key key; _token token; _expiry expiry; _owner owner; _depth 1; // 开启看门狗续期任务 _renewalTask StartRenewalLoop(); } public void IncrementDepth() Interlocked.Increment(ref _depth); private Task StartRenewalLoop() { return Task.Run(async () { var interval TimeSpan.FromMilliseconds(_expiry.TotalMilliseconds / 3); try { while (!_cts.IsCancellationRequested) { await Task.Delay(interval, _cts.Token); if (_cts.IsCancellationRequested) { break; } long renewed await TryRenewAsync(); if (renewed 0) { // 锁已经不在自己手上了停止续期 break; } } } catch (OperationCanceledException) { // 正常取消不处理 } }); } private async Tasklong TryRenewAsync() { // 续期脚本只有 value 仍等于自己的 token 时才重新设置过期时间 const string script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(pexpire, KEYS[1], ARGV[2]) end return 0 ; var result await _db.ScriptEvaluateAsync( script, new RedisKey[] { _key }, new RedisValue[] { _token, (long)_expiry.TotalMilliseconds }); return (long)result; } public async ValueTask DisposeAsync() { // 可重入锁场景内部申请也释放一次但锁要等所有层次都释放后才真正删除 if (Interlocked.Decrement(ref _depth) 0) { return; } _cts.Cancel(); try { await _renewalTask; } catch (OperationCanceledException) { } const string script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) end return 0 ; await _db.ScriptEvaluateAsync( script, new RedisKey[] { _key }, new RedisValue[] { _token }); _owner.RemoveBucket(_key, this); } }这个类有几点需要重点理解。看门狗续期的间隔是expiry / 3这是一个常用的保守策略。如果锁过期时间是 30 秒那么每 10 秒续期一次即使某一次续期因为网络抖动失败下一次续期仍然有机会把锁续回来不至于让锁在业务中途消失。续期脚本和卸锁脚本都严格遵守“先比较 token再修改过期时间 / 删除 key”的原则避免因为锁超时后已转交给其他请求本请求还继续操作 Redis key导致他人锁被误删。4.4 RedisDistributedLock加锁与可重入上下文然后实现锁服务的主类Infra/RedisDistributedLock.csusing StackExchange.Redis; namespace OrderApi.Infra; public sealed class RedisDistributedLock { private readonly IDatabase _db; private readonly TimeSpan _defaultExpiry; // AsyncLocal 保证同一个异步上下文内共享一份可重入锁记录 private readonly AsyncLocalDictionarystring, LockLease? _reentrantContext new(); public RedisDistributedLock(IDatabase db, TimeSpan defaultExpiry) { _db db; _defaultExpiry defaultExpiry; } public async TaskLockLease? AcquireAsync( string lockKey, string? token null, TimeSpan? expiry null, CancellationToken cancellationToken default) { token ?? Guid.NewGuid().ToString(N); expiry ?? _defaultExpiry; var bucket _reentrantContext.Value ?? new Dictionarystring, LockLease(StringComparer.Ordinal); // 同一个异步上下文已经持有该锁直接重入 if (bucket.TryGetValue(lockKey, out var existingLease)) { existingLease.IncrementDepth(); return existingLease; } bool isLocked await _db.StringSetAsync(lockKey, token, expiry.Value, When.NotExists); if (!isLocked) { return null; } var lease new LockLease(_db, lockKey, token, expiry.Value, this); bucket[lockKey] lease; return lease; } internal void RemoveBucket(string lockKey, LockLease lease) { var bucket _reentrantContext.Value; if (bucket null) { return; } if (bucket.TryGetValue(lockKey, out var current) ReferenceEquals(current, lease)) { bucket.Remove(lockKey); } } }这里的可重入实现思路和传统基于线程维度的可重入锁略有不同。AsyncLocal保存的是当前异步上下文中的锁记录。在一个 WebAPI 请求链路里无论经过多少次await只要没有显式开启新的线程或新的 Task 上下文锁记录就能一直传递下去所以同一请求内重复申请同一把锁时可以直接命中重入逻辑不会阻塞。当然也需要说明如果你在代码里主动使用Task.Run或new Thread把业务丢到另一个线程去执行AsyncLocal不会自动传递已经发生变更的值此时可重入锁会失效。大多数 WebAPI 场景不会出现这种情况就算出现了也只是退化为重新向 Redis 申请锁不会造成死锁只是多一次 Redis 交互。4.5 编写订单业务服务业务层使用一个自定义的OrderService来模拟“查询库存 扣减库存”的共享资源操作。这里故意把“判断”和“扣减”拆成两步目的是演示分布式锁的价值锁保证同一商品 ID 的请求串行执行。using System.Collections.Concurrent; namespace OrderApi.Services; public sealed class OrderService { // 模拟共享存储生产环境中请使用数据库行锁、乐观锁或 Redis 原子操作兜底 private readonly ConcurrentDictionarystring, int _stocks new() { [p-1001] 10, [p-1002] 5 }; public async Taskbool TryDecreaseStockAsync(string productId, int count) { if (!_stocks.TryGetValue(productId, out var stock)) { return false; } if (stock count) { return false; } // 模拟耗时操作例如远程服务调用、写数据库等 await Task.Delay(5000); _stocks[productId] stock - count; return true; } public int GetStock(string productId) { return _stocks.TryGetValue(productId, out var stock) ? stock : 0; } }这里使用了ConcurrentDictionary来避免不同商品 ID 的并发请求导致字典内部结构损坏。同一个商品 ID 的并发请求则通过外部 Redis 锁串行化所以我们能看到“判断 延迟 扣减”这个复合操作被保护得很好。实际生产中真正扣库存的 SQL 通常还会带上stock count条件UPDATE t_stock SET stock stock - count WHERE product_id productId AND stock count;这样即使锁失效数据库层面也有最后一道防线。分布式锁解决的是“复合操作无法一步原子完成”的问题数据库条件更新解决的是“单条数据并发写入”的问题两者是互补关系。4.6 控制器锁的获取与释放接下来是Controllers/OrdersController.csusing Microsoft.AspNetCore.Mvc; using OrderApi.Infra; using OrderApi.Services; namespace OrderApi.Controllers; [ApiController] [Route(api/orders)] public sealed class OrdersController : ControllerBase { private readonly RedisDistributedLock _distributedLock; private readonly OrderService _orderService; private readonly ILoggerOrdersController _logger; public OrdersController( RedisDistributedLock distributedLock, OrderService orderService, ILoggerOrdersController logger) { _distributedLock distributedLock; _orderService orderService; _logger logger; } [HttpPost(create)] public async TaskIActionResult CreateOrderAsync([FromQuery] string productId, [FromQuery] int count) { var lockKey $order:lock:{productId}; var lease await _distributedLock.AcquireAsync(lockKey); if (lease is null) { return Conflict(new { code 429, message 当前操作人数较多请稍后重试, lockKey }); } await using var safeLease lease; bool success await _orderService.TryDecreaseStockAsync(productId, count); if (!success) { return BadRequest(new { code 400, message 库存不足, remain _orderService.GetStock(productId) }); } _logger.LogInformation(下单成功 ProductId{ProductId}, Count{Count}, Remain{Remain}, productId, count, _orderService.GetStock(productId)); return Ok(new { code 0, message 下单成功, data new { productId, count, remain _orderService.GetStock(productId) } }); } [HttpGet(reentrant-test)] public async TaskIActionResult ReentrantTestAsync([FromQuery] string productId) { var lockKey $order:lock:{productId}; var outerLease await _distributedLock.AcquireAsync(lockKey); if (outerLease is null) { return Conflict(new { code 429, message 获取锁失败 }); } await using var outer outerLease; // 同一请求链路内再次申请同一把锁验证可重入 var innerLease await _distributedLock.AcquireAsync(lockKey); if (innerLease is null) { return Problem(可重入锁实现有误第二次获取同一把锁失败); } await using var inner innerLease; return Ok(new { code 0, message 可重入锁验证通过, data new { lockKey, outerToken outerLease.Token, innerSameToken innerLease.Token outerLease.Token } }); } }需要注意AcquireAsync返回 null 表示加锁失败此时我们没有重试而是直接返回 429 Conflict。真实项目中如果业务允许可以在客户端做指数退避重试也可以引入队列化处理例如先写入消息队列再异步消费。4.7 注册依赖并启动最后在Program.cs中完成依赖注入using OrderApi.Infra; using OrderApi.Services; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var redisOptions builder.Configuration .GetSection(Redis) .GetRedisOptions() ?? new RedisOptions(); builder.Services.AddSingleton(new RedisService(redisOptions.ConnectionString)); builder.Services.AddSingleton(sp { var redisService sp.GetRequiredServiceRedisService(); return new RedisDistributedLock( redisService.Database, TimeSpan.FromSeconds(redisOptions.LockExpirySeconds)); }); builder.Services.AddSingletonOrderService(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();启动项目dotnet run然后使用 Postman 或浏览器发送请求POST http://localhost:5000/api/orders/create?productIdp-1001count1由于OrderService中故意加了Task.Delay(5000)接口会在 5 秒后返回。在 Redis 可视化工具中可以看到order:lock:p-1001这个 key 在业务执行期间一直存在并且 TTL 始终在 30 秒左右被刷新这就是看门狗续期在起作用的结果。可以使用 Postman 的 Runner 功能同时发送 10 个请求观察返回值。正常结果是第一个请求拿到锁并成功下单其余请求在锁未释放时到达直接返回 429 冲突说明锁的互斥生效了。当整个压测结束后Redis 中的锁 key 会被自动删除。5. 常见问题与排查思路5.1 高频问题排查表问题现象常见原因解决思路多个线程同时进入临界区锁 key 设置遗漏了NX或使用的是SETNX后手动EXPIRE中间进程退出导致锁失效统一使用SET key value NX EX确保原子性业务执行中锁提前释放锁过期时间设置太短没有看门狗续期增加锁过期时间或实现看门狗自动续期释放锁时误删了别人的锁解锁前没有校验 value 是否为当前客户端持有的 token使用 Lua 脚本先比较GET key token再DEL保证原子性锁获取成功后突然崩溃锁无法释放客户端崩溃前没有执行释放锁逻辑加锁时设置合理的过期时间保证崩溃后锁自动过期可重入申请同一把锁时死锁或失败没有实现重入计数第二次加锁把自己阻塞在客户端上下文记录已持有锁重入时递增计数退出时递减Redis 主从切换后锁丢失主节点未将锁数据同步到从节点从节点升级后丢失锁接受短暂锁丢失风险由数据库乐观锁兜底极端可靠性需求再考虑 RedLock锁冲突率极高大量请求返回 429锁粒度太大或临界区耗时过长缩小锁粒度如按用户、商品、订单维度拆分锁将阻塞操作移出临界区5.2 锁超时时间如何设置锁超时时间没有标准答案需要结合业务实际执行时间评估。基本原则是锁超时时间 ≥ 业务最坏执行时间。如果业务平均执行 200 毫秒最坏执行 2 秒那么锁超时设置为 5~10 秒是比较合理的。引入看门狗后锁超时时间不必追求极度精确因为续期机制会在业务未完成时自动延长锁的生存时间。但要注意看门狗续期并不是万能的。如果持有锁的进程发生长时间 GC 停顿、网络分区、进程卡死看门狗线程同样可能无法执行续期锁最终还是会被释放。这时业务侧要设计重试或幂等逻辑而不是寄希望于锁永远不失效。5.3 看门狗还在锁却已经没了这是一种比较隐蔽的情况看门狗续期是独立的后台任务而业务线程可能因为阻塞导致 Redis 连接超时但看门狗线程依然在跑。实际上如果锁已经因为某种原因被删除续期脚本中的GET key token判断会失败看门狗会主动停止续期不会出现“锁没了还继续续命”的问题。更需要注意的反而是 NetCore 中异步上下文的问题。如果业务代码里跨了Task.Run导致锁的 token 没有在同一个上下文中传播可重入逻辑失效就会多申请一次锁而 Redis 锁本身是按 key 互斥的第二次申请如果失败就直接返回 429。遇到这种场景时优先检查锁对象是否被跨上下文传递。6. 生产环境最佳实践6.1 原子性是底线分布式锁最容易出问题的环节永远是加锁和解锁的原子性。加锁必须使用SET NX EX解锁必须使用 Lua 脚本。任何“先查询再操作”的两段式写法都要视为潜在 Bug。如果团队内其他人也要使用分布式锁建议把锁的获取、续期、释放封装成独立的RedisDistributedLock类所有业务代码在统一的 API 上工作而不是直接写StringSetAsync。6.2 锁粒度与持锁时间锁粒度要尽可能小。能按商品 ID 加锁就不要按“商品分类”加锁能按用户 ID 加锁就不要全局加锁。锁粒度越小系统的并发能力越高。持锁时间要尽可能短。不要在锁里面调用外部慢接口、不要做大批量数据库查询、不要把请求体的完整流程都放在锁里。锁只保护“必须要串行执行的复合操作”其他无关操作一律放在锁外面。一种常见的优化方法是将“真正需要互斥的最小操作”提取成一个独立方法锁只包住这个方法var lease await _distributedLock.AcquireAsync(lockKey); await using var safeLease lease; // 锁内只做核心操作 await _orderRepository.DecreaseStockAsync(productId, count);日志、监控上报、消息通知等操作放在锁外异步执行。6.3 监控、降级与容灾分布式锁不是银弹Redis 本身也可能出现故障。生产环境要关注三个指标锁获取成功率、锁获取等待时间、锁冲突率。Redis 不可用时可以采用降级策略。简单项目可以在加锁失败后直接返回错误提示重要项目建议加入熔断例如连续 N 次加锁失败后将分布式锁暂时降级为数据库乐观锁或本地限流避免业务完全不可用。另外不要把 Redis 分布式锁和数据库的唯一约束、乐观锁对立起来。合理的方案是“Redis 锁负责请求串行化数据库约束负责最终兜底”。即使 Redis 锁偶尔失效数据库层也能拦住重复请求。6.4 RedLock 与主从切换Redis 官方提出 RedLock 是为了解决“主节点写入锁成功后数据尚未同步到从节点主节点就宕机从节点晋升后锁丢失”的问题。RedLock 在多节点独立部署、quorum 判定等细节上比较复杂。对于大多数读多写少、对并发一致性容忍度有限的业务直接使用单机 Redis 或 Redis Cluster 数据库兜底反而是更务实的做法。RedLock 是否要引入应该由业务对“锁丢失风险”的容忍度决定而不是为了面试知识点强行引入。6.5 注意锁的命名规范锁 key 的命名直接影响排障效率。推荐使用冒号分隔的语义化命名业务:锁类型:目标ID示例order:lock:p-1001 coupon:lock:user-9527 job:lock:settle-20241105统一格式的 key 在 Redis Desktop Manager 中更容易检索也方便批量清理过期测试数据。7. 结语分布式锁是后端的基本功AI 辅助编程能帮你写出“获取锁的代码”但写不出对并发边界条件的判断。真正上线前要能回答自己这几个问题加锁是不是原子的解锁会不会误删别人的锁业务没跑完锁会不会提前释放同一个请求再次申请锁会不会死锁Redis 出故障时业务怎么办把这几个问题全部解决才算是一个生产级分布式锁方案。本文实现的RedisDistributedLockLockLease组合覆盖了SET NX EX加锁、Lua 脚本安全解锁、看门狗自动续期、基于 AsyncLocal 的可重入语义可以直接复制到你的 .NET 8 / .NET 10 WebAPI 项目中继续改造。建议你动手跑一遍并发测试观察 Redis 中 key 的 TTL 变化再尝试把锁超时时间改成 2 秒看续期效果会比单纯看文章理解深刻得多。