.NET 10 WebAPI实战:Redis分布式锁解决高并发超卖与重复请求
发布时间:2026/9/8 13:23:30 作者:尧图编辑部 阅读量:1,286

这次我们来看一套 .NET 10 WebAPI 高并发后端架构实战。主题很明确用 Redis 实现分布式锁解决集群部署下最常见的三类问题——并发冲突、超卖、重复请求。你可以把它理解成一套既能跟着敲代码也能直接拿回项目里改造的技术模板。先说这串技术点的实际价值。单体应用切到多实例部署后原本靠进程内 Lock 锁住的代码段会全部失效。A 实例和 B 实例同时扣减同一件商品的库存各自都以为库存够用最后超卖。前端用户手抖点了两次提交两个实例同时收到请求订单重复创建。这些问题不是靠堆机器能解决的必须在多个服务节点之间建立一把“共享锁”也就是分布式锁。Redis 分布式锁是这套方案里性价比最高的实现方式。如果你正在做 .NET 服务拆分、多实例部署或者想系统入门高并发架构这篇文章建议收藏。本文会带你把 WebAPI 项目建起来把 Redis 跑起来把分布式锁写进关键接口再用并发请求验证效果最后给出一套可落地的排查清单和最佳实践。1. 核心能力速览能力项说明项目类型.NET 10 WebAPI 后端服务实战核心功能基于 Redis 分布式锁解决集群并发冲突、库存超卖、重复请求技术栈.NET 10 WebAPI、StackExchange.Redis、Redis 7、RedLock.net可选、后台任务支持平台Windows / Linux / Docker启动方式dotnet CLI、Visual Studio、Docker Compose主要验证场景下单接口并发测试、幂等去重、定时任务抢锁批量任务能力可通过后台服务 分布式锁实现跨节点互斥的定时任务API 接口标准 RESTful WebAPI支持 JSON 返回硬件要求开发机常规配置即可Redis 可本地运行无需 GPU适合场景多实例部署的 .NET 服务、电商库存扣减、订单幂等、分布式任务调度这里需要说明一点标题里的“全套源码教学”指的是从零搭建的完整代码演示过程不是甩给你一个看不懂的巨型仓库。本文会把关键的分布式锁代码、Redis 接入代码、接口代码和并发测试代码全部展开你照着写就能跑通。2. 适用场景与使用边界先说清这套方案适合谁。第一类是还在单体架构、但准备做多实例部署的 .NET 团队。这类团队最常见的痛点是代码已经写好性能测试发现单机扛不住一扩容就出超卖和重复订单。分布式锁是解决这类问题的入门必学方案。第二类是已经在做微服务拆分、需要保护跨服务临界资源的团队。比如订单服务和库存服务分离后一次下单要跨服务操作单纯靠数据库 UPDATE 语句已经不够需要分布式锁来协调。第三类是面试前需要系统梳理 Redis 分布式锁知识点的后端开发。这类读者会特别关注锁的实现细节、缓存失效场景和 RedLock 争议。但也要说清边界。分布式锁不是万能的下面这些场景可能不适合单纯扣库存的超级高频操作优先考虑 Redis Lua 原子脚本而不是加锁。锁本身会带来上下文切换开销加锁、等待、解锁三个动作都是有成本的。需要强一致性的金融级业务分布式锁只能作为前置拦截最终一致性要靠数据库事务、唯一约束和状态机兜底。Redis 主从切换场景下锁可能丢失。如果业务对锁的可靠性要求极高要考虑 RedLock 方案或者引入数据库锁作为兜底。版权、隐私和安全边界也必须提一下。这套代码演示的是标准电商下单和任务调度逻辑不要在实际业务中把身份证、手机号、支付明文等敏感信息直接拼到 Redis key 或日志里。涉及用户数据、人脸、声音等素材必须确认合法授权。分布式锁只解决并发互斥问题不解决数据合规问题。3. 环境准备与前置条件3.1 安装 .NET 10 SDK开发机上需要安装 .NET 10 SDK。如果你本机版本不是最新的也不用太紧张本文的核心代码在 .NET 8 / .NET 9 上也可以正常编译运行只有极少数的框架 API 差异需要微调。安装完成后可以在命令行确认版本dotnet --version建议使用 Visual Studio 2022 及以上版本或者 JetBrains Rider。VS 2022 需要确保安装了 ASP.NET 和 Web 开发工作负载否则可能无法直接创建 WebAPI 项目。3.2 准备 Redis 环境Redis 是这套架构的锁存储核心。有三种常见安装方式方式一Windows 开发机安装。Windows 下可以使用 Redis 官方提供的 MSI 安装包也可以使用 Memurai 这类 Redis 兼容实现。安装完成后启动服务redis-server.exe redis.windows.conf方式二Docker 快速启动。如果机器上有 Docker推荐直接用容器干净且好卸载docker run --name redis7 -p 6379:6379 -d redis:7方式三Linux 服务器安装。生产环境一般使用 Linux通过包管理工具安装 Redis 后用 systemd 管理服务。启动后建议安装一个 Redis 可视化客户端比如 Redis Desktop Manager 或 Another Redis Desktop Manager。后面验证分布式锁时可以直接看到锁 key 是否创建、TTL 剩余时间、以及锁释放后 key 是否被删除。3.3 确认端口和依赖WebAPI 默认端口一般在 5000 到 5300 之间Redis 默认端口是 6379。启动前检查端口是否被占用netstat -ano | findstr 6379 netstat -ano | findstr 5000如果端口被占用后续可以在启动配置里修改。4. 创建 WebAPI 项目与 Redis 接入4.1 创建项目打开命令行执行以下命令创建项目dotnet new webapi -n Net10.Redis.Lock.Demo cd Net10.Redis.Lock.Demo然后添加两个 NuGet 包。第一个是 Redis 客户端 StackExchange.Redis第二个是 RedLock.net后者用于多 Redis 节点的高可靠锁场景可以先安装备用dotnet add package StackExchange.Redis dotnet add package RedLock.net4.2 配置 Redis 连接字符串打开appsettings.json加入 Redis 连接配置{ ConnectionStrings: { Redis: 127.0.0.1:6379,password,abortConnectfalse,connectTimeout5000,defaultDatabase0 }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }这里把abortConnect设置成false避免 Redis 暂时不可用时程序直接启动失败。connectTimeout设置成 5000 毫秒防止连接超时时间过短导致误报。4.3 注册 Redis 连接服务打开Program.cs注册 IConnectionMultiplexer 单例。这个对象是线程安全的整个进程复用同一个连接即可using StackExchange.Redis; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingletonIConnectionMultiplexer(sp { var configuration builder.Configuration.GetConnectionString(Redis); return ConnectionMultiplexer.Connect(configuration!); }); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();到这里WebAPI 已经能启动并且连接 Redis 的通道已经打通。接下来是关键部分写分布式锁。5. 分布式锁实现与核心场景5.1 基于 SETNX EX 的原子加锁Redis 分布式锁最经典的实现思路是使用SET key value NX EX timeout命令只有 key 不存在时才设置成功并且自动设置过期时间。NX保证互斥EX保证即使服务宕机锁也会自动释放不会死锁。锁的 value 必须是唯一 token不能是固定值否则可能误删其他请求持有的锁。在 StackExchange.Redis 中对应的方法是StringSetAsync并传入When.NotExists条件。下面写一个通用的 RedisLockServiceusing StackExchange.Redis; namespace Net10.Redis.Lock.Demo.Services; public sealed class RedisLockService { private readonly IConnectionMultiplexer _redis; public RedisLockService(IConnectionMultiplexer redis) { _redis redis; } public async TaskIDisposable AcquireAsync(string resource, string token, TimeSpan expiry) { var db _redis.GetDatabase(); bool acquired await db.StringSetAsync( key: resource, value: token, expiry: expiry, when: When.NotExists ); if (!acquired) { throw new InvalidOperationException(资源正被其他请求处理请稍后重试); } return new ReleaseLock(db, resource, token); } private sealed class ReleaseLock : IDisposable { private readonly IDatabase _db; private readonly string _key; private readonly string _token; public ReleaseLock(IDatabase db, string key, string token) { _db db; _key key; _token token; } public void Dispose() { string script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; _db.ScriptEvaluate(script, new RedisKey[] { _key }, new RedisValue[] { _token }); } } }这段代码有两个细节值得注意。第一释放锁时必须用 Lua 脚本来“先比较 value 再删除”而不是直接KeyDeleteAsync。因为如果锁已经过期当前请求的临界区还没执行完另一个请求已经拿到锁并开始操作。此时当前请求释放锁只应该释放自己的锁而不能误删别人的锁。第二token由调用方生成一般用Guid.NewGuid().ToString(N)。这样每个请求拿到的锁 value 都不同释放锁时能精确匹配。5.2 注册锁服务在Program.cs中注册builder.Services.AddSingletonRedisLockService();5.3 场景一解决库存超卖超卖的本质是“查询库存”和“扣减库存”不是原子操作。在多实例部署下两个实例同时读到库存为 1都认为可以下单最后实际扣了两次库存变成 -1。使用分布式锁把“检查库存 - 扣减库存 - 写入订单”这段业务临界区保护起来保证同一商品同一时间只有一个请求可以执行。下面是一个简化的下单接口[ApiController] [Route(api/orders)] public class OrderController : ControllerBase { private readonly IConnectionMultiplexer _redis; private readonly RedisLockService _lockService; public OrderController(IConnectionMultiplexer redis, RedisLockService lockService) { _redis redis; _lockService lockService; } [HttpPost] public async TaskIActionResult CreateOrder(CreateOrderRequest request) { if (string.IsNullOrEmpty(request.RequestId)) { return BadRequest(new { code 400, message RequestId 不能为空 }); } string lockKey $order:create:{request.ProductId}:{request.RequestId}; string token Guid.NewGuid().ToString(N); try { using (await _lockService.AcquireAsync(lockKey, token, TimeSpan.FromSeconds(10))) { var db _redis.GetDatabase(); // 1. 检查这个请求是否已经处理过实现幂等 bool exists await db.KeyExistsAsync($order:dup:{request.RequestId}); if (exists) { return Ok(new { code 200, message 重复请求已拦截 }); } // 2. 模拟数据库库存扣减这里以 Redis 字符串演示 long stock await db.StringIncrementAsync($stock:item:{request.ProductId}, -request.Quantity); if (stock 0) { // 库存不足回滚 await db.StringIncrementAsync($stock:item:{request.ProductId}, request.Quantity); return BadRequest(new { code 400, message 库存不足 }); } // 3. 写入订单数据实际项目中这里会是数据库操作 // await _orderService.CreateAsync(request); // 4. 记录幂等标记设置合理的过期时间 await db.StringSetAsync($order:dup:{request.RequestId}, 1, TimeSpan.FromHours(24)); return Ok(new { code 200, message 下单成功, stock }); } } catch (InvalidOperationException) { return Conflict(new { code 409, message 请求处理中请勿重复提交 }); } } } public record CreateOrderRequest(string ProductId, int Quantity, string RequestId);这里用 Redis 的StringIncrementAsync模拟扣库存并配合回滚。实际生产项目中库存数据通常放在数据库里核心逻辑是类似的先加锁再在锁内执行数据库事务。加锁保证了同一商品在集群环境下只有一个请求进入临界区数据库事务保证了数据一致性。5.4 场景二解决重复请求重复请求和超卖经常一起出现。用户快速点击两次提交按钮或前端超时重试都会导致同一个业务请求被发送多次。解决思路是引入 RequestId。前端在发起请求时生成一个唯一请求 ID后端用RequestId作为幂等标记。处理过第一次请求后把标记写入 Redis 并设置过期时间第二次请求进入时直接拦截。上面的代码已经包含了这个逻辑bool exists await db.KeyExistsAsync($order:dup:{request.RequestId}); if (exists) { return Ok(new { code 200, message 重复请求已拦截 }); }这里要跟分布式锁配合理解分布式锁解决的是“同一时刻只有一个实例可以操作”的问题幂等标记解决的是“重复请求在业务结果上不产生副作用”的问题。两者搭配使用效果最好。5.5 更优的库存扣减实现Lua 脚本分布式锁能解决超卖但每一次库存扣减都要经历“加锁、查询库存、扣减、解锁”的完整流程。如果库存扣减本身只是单个 Redis 操作用 Lua 脚本更高效。Redis 的 Lua 脚本可以保证多个操作在服务端原子执行。下面是一个扣减库存的 Lua 脚本local stock redis.call(get, KEYS[1]) if not stock then return -1 end local num tonumber(stock) local decr tonumber(ARGV[1]) if num decr then return -2 end redis.call(decrby, KEYS[1], decr) return num - decr在 .NET 中调用var db _redis.GetDatabase(); var script local stock redis.call(get, KEYS[1]) if not stock then return -1 end local num tonumber(stock) local decr tonumber(ARGV[1]) if num decr then return -2 end redis.call(decrby, KEYS[1], decr) return num - decr ; var result await db.ScriptEvaluateAsync(script, new RedisKey[] { stock:item:P1001 }, new RedisValue[] { 1 });Lua 脚本返回 -1 表示商品不存在返回 -2 表示库存不足返回其余值表示扣减后的剩余库存。这种方案比分布式锁的开销更小适合纯 Redis 场景。那什么时候用分布式锁答案是当临界区不只是 Redis 操作而是跨数据库、跨服务、跨消息队列的多步业务时。比如先查数据库库存再调支付服务再写订单表最后发消息。这种情况下 Lua 脚本管不了外部系统需要分布式锁把整段逻辑保护起来。5.6 RedLock 高可靠锁扩展如果公司部署了多个独立的 Redis 主节点为了降低“某个主节点宕机导致锁丢失”的风险可以使用 RedLock 算法。.NET 中对应 RedLock.net 这个 NuGet 包。RedLock 的思路是向多个 Redis 主节点同时申请锁只有超过半数节点加锁成功才算获取锁成功。以下是一个基本使用示例using RedLockNet.SERedis; using RedLockNet.SERedis.Configuration; var multiplexers new ListRedLockMultiplexer { await ConnectionMultiplexer.ConnectAsync(redis1:6379), await ConnectionMultiplexer.ConnectAsync(redis2:6379), await ConnectionMultiplexer.ConnectAsync(redis3:6379) }; var redlockFactory RedLockFactory.Create(multiplexers); await using var redLock await redlockFactory.CreateLockAsync( resource: order:create:P1001, expiryTime: TimeSpan.FromSeconds(10), waitTime: TimeSpan.FromSeconds(2), retryTime: TimeSpan.FromMilliseconds(100) ); if (redLock.IsAcquired) { // 执行业务逻辑 }需要提醒一句RedLock 在分布式系统领域一直存在争议部分专家认为它在大规模复杂网络环境下也无法保证绝对的安全。更稳妥的做法是把 RedLock 当作一种高可用选择而不是唯一防线。业务层面仍要做幂等和数据库兜底。6. 接口测试与并发模拟6.1 启动服务在项目目录下执行dotnet run启动后如果配置了 Swagger可以直接访问 Swagger 页面测试接口。也可以使用 Postman、Apifox 等工具请求接口。默认接口地址通常类似http://localhost:5xxx/api/orders具体端口以启动日志为准。6.2 准备测试数据下单前先给商品初始化库存。可以使用 Redis 客户端可视化工具执行命令SET stock:item:P1001 5这条命令把商品 P1001 的初始库存设置为 5。后面所有并发测试都围绕这个 key 展开。6.3 并发请求测试下面写一个 PowerShell 脚本模拟 20 个并发请求同时下单每个请求使用不同的 RequestId$baseUrl http://localhost:5210/api/orders $tasks 1..20 | ForEach-Object { $body { productId P1001 quantity 1 requestId [guid]::NewGuid().ToString() } | ConvertTo-Json Invoke-RestMethod -Uri $baseUrl -Method Post -Body $body -ContentType application/json } $results $tasks | ForEach-Object { $_ } $success ($results | Where-Object { $_.code -eq 200 }).Count $fail ($results | Where-Object { $_.code -ne 200 }).Count Write-Host 成功: $success, 失败: $fail如果分布式锁生效库存为 520 个并发请求中最多只有 5 个成功其余请求会因为库存不足返回失败。同时如果使用相同的 RequestId 重复请求后进入的请求会被幂等标记拦截。6.4 在 Redis 可视化客户端中观察锁在并发测试过程中打开 Redis Desktop Manager 或 Another Redis Desktop Manager观察order:create:*前缀的 key。正常情况下可以看到锁 key 在请求处理期间存在处理结束后被 Lua 脚本删除。如果服务在持锁期间崩溃锁 key 会在设置的过期时间后自动消失不会变成死锁。“锁 key 是否按预期过期、是否被正确删除”是判断分布式锁实现是否规范的重要标准。6.5 高并发压测建议如果要进一步压测可以使用 APISIX、JMeter、wrk、abApache Bench等工具。对下单接口关注两个指标请求响应时间分布。成功下单数和库存扣减数是否一致。压测时一定要开日志。每次加锁成功、加锁失败、库存扣减成功、库存不足都记录下来。否则并发问题出现时很难定位是第一把锁没加上还是业务代码事务没有提交。7. 后台任务与批量任务中的分布式锁高并发场景下除了用户请求还有一种典型的并发冲突来源后台任务。假设你部署了 3 个 WebAPI 实例每个实例都有订单超时扫描任务。如果没有分布式锁3 个实例会同时扫描订单重复处理同一批超时订单产生重复通知、重复退款等问题。解决思路是在后台任务执行前先抢分布式锁抢到锁的实例才执行任务其他实例直接跳过或等待下一轮。下面是一个标准的 BackgroundService 示例using StackExchange.Redis; namespace Net10.Redis.Lock.Demo.Workers; public sealed class OrderTimeoutScanWorker : BackgroundService { private readonly IConnectionMultiplexer _redis; private readonly ILoggerOrderTimeoutScanWorker _logger; private const string LockKey worker:order-timeout-scan:lock; private static readonly TimeSpan LockExpiry TimeSpan.FromMinutes(2); public OrderTimeoutScanWorker(IConnectionMultiplexer redis, ILoggerOrderTimeoutScanWorker logger) { _redis redis; _logger logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var db _redis.GetDatabase(); string token Guid.NewGuid().ToString(N); bool acquired await db.StringSetAsync(LockKey, token, LockExpiry, When.NotExists); if (acquired) { try { _logger.LogInformation(开始扫描超时订单); await ScanExpiredOrdersAsync(stoppingToken); } finally { await ReleaseLockAsync(db, LockKey, token); } } else { _logger.LogInformation(其他实例正在执行扫描本轮跳过); } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } private static async Task ReleaseLockAsync(IDatabase db, string key, string token) { string script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; await db.ScriptEvaluateAsync(script, new RedisKey[] { key }, new RedisValue[] { token }); } private Task ScanExpiredOrdersAsync(CancellationToken stoppingToken) { // 实际业务逻辑查询超时订单、发送通知、更新状态 return Task.CompletedTask; } }这里有几个处理细节锁过期时间不能乱设。如果扫描任务经常超过锁过期时间要手动续期或者把过期时间调整到覆盖大多数执行时长的范围。Finally中释放锁保证正常执行和异常执行都能释放。释放时同样使用 Lua 脚本校验 token防止误删其他实例的锁。每轮任务间隔 5 秒避免频繁抢锁给 Redis 造成压力。批量任务也可以沿用这个模型比如批量导出报表、批量重算积分、批量推送消息。核心原则是一个集群中同一时间只允许一个节点处理同一类任务。8. 资源占用与性能观察8.1 观察 Redis 资源分布式锁本身占用的 Redis 内存非常小每个锁 key 的体量通常只有几十字节主要成本来自锁的创建和释放频率。高并发场景下每秒钟可能有成千上万次加锁和解锁操作这会占用 Redis 的连接和 CPU。可以使用 Redis 自带的 INFO 命令查看运行状态redis-cli INFO重点看这些指标connected_clients当前连接数判断连接是否存在泄漏。used_memoryRedis 内存使用量锁 key 不是主要内存消耗但幂等标记会积累大量 key需要注意过期时间是否合理。keyspace_hits和keyspace_misses命中率判断缓存数据是否过于频繁失效。如果使用可视化客户端可以直接观察数量最多的 key 前缀。order:dup:*这类幂等 key 如果不设置合理的过期时间会持续堆积最终变成 Redis 内存膨胀的隐患。8.2 观察 .NET 进程资源WebAPI 进程的资源占用可以通过任务管理器、dotnet-counters、容器监控等工具查看。分布式锁在高并发下主要影响以下几个指标CPU加锁、解锁涉及 Redis 网络往返对 CPU 的消耗不算高但如果频繁抢锁失败并重试CPU 会明显上升。网络连接数StackExchange.Redis 默认是多路复用连接正常情况下一个进程不会因为并发量而无限增加连接。如果连接数异常增长检查是否有 Redis 连接对象被频繁创建。线程池活跃线程锁等待和网络 IO 会占用异步线程阻塞逻辑写多了会拉高线程池压力。8.3 关键性能观察点不要一次性把所有并发压上去。建议按梯度测试10 并发、50 并发、100 并发、500 并发每轮记录下面的结果观察维度关注内容加锁成功率是否出现大量冲突是否在预期范围锁等待耗时从请求进入到锁内执行的时间锁持有时间临界区代码平均执行时间失败重试情况加锁失败后请求是否立即返回还是重试库存扣减准确性成功下单数和库存扣减数是否完全一致幂等拦截效果相同 RequestId 的请求是否被拦截资源占用Redis 内存、连接数、WebAPI 进程 CPU/内存每次压测后清理测试数据尤其要把测试用的幂等 key 清掉否则后续测试会因为前面的标记而误判重复请求。9. 常见问题与排查方法问题现象可能原因排查方式解决方案程序启动后提示 Redis 连接失败Redis 未启动、端口错误、密码错误、防火墙阻止在命令行执行redis-cli ping确认 Redis 返回 PONG启动 Redis检查连接字符串和防火墙确认密码配置正确接口请求返回“请勿重复提交”前一次请求持有锁未释放或锁过期时间过长用 Redis Desktop Manager 查看order:create:*的 TTL检查业务临界区是否耗时过长调整锁过期时间确认 finally 中释放锁锁释放后其他请求仍拿不到锁锁 key 被误删除或 token 不匹配导致删除失败观察 Redis key 的 value 和当前请求的 token确认释放锁时使用 Lua 脚本校验 value检查是否手动执行了 DEL 命令并发测试后库存出现负数扣库存操作没有严格在锁内执行或数据库兜底缺失查看日志确认库存扣减是否在锁获取之后把库存扣减逻辑挪进锁临界区增加 Lua 脚本原子扣减数据库 UPDATE 增加库存大于等于数量的条件相同 RequestId 的重复请求未被拦截幂等 key 过期时间太短或幂等检查放在锁之外执行发现幂等 key 已过期或请求绕过了幂等检查增加幂等 key 的过期时间把幂等检查放在锁临界区的第一步后台任务每个实例都在执行后台任务没有参与抢锁或锁释放后其他实例立即执行查看日志是否有“抢锁成功”记录在任务执行前调用StringSetAsync加锁只有 acquired 为 true 才继续执行压测时 WebAPI 响应变慢锁等待时间过长或 Redis 连接池阻塞监控 Redis 的 connected_clients 和命令耗时缩短临界区代码执行时间加锁失败后采用快速失败策略必要时拆分锁粒度重启 Redis 后锁丢失锁 key 未持久化或 Redis 配置为不持久化确认 RDB/AOF 配置生产环境开启 Redis 持久化锁丢失场景由数据库幂等兜底10. 最佳实践与工程落地建议10.1 锁命名规范锁 key 要包含业务前缀和资源 ID方便排错和监控。推荐格式业务名:资源名:资源ID例如order:create:P1001 worker:order-timeout-scan:lock不建议直接用“lock1”“key1”这种无业务含义的名字。出了问题你根本分不清是哪段代码在持有这把锁。10.2 锁过期时间锁过期时间太短临界区代码还没执行完锁就自动释放了其他请求趁虚而入。锁过期时间太长如果持锁服务崩溃其他请求要等很久才能拿到锁。简单有效的做法是先记录临界区代码正常执行消耗的时间设置锁过期时间为最大耗时的 3 到 5 倍。对于长时间运行的批量任务可以在持有锁期间动态续期。10.3 加锁失败的处理策略加锁失败不要直接抛异常先判断业务场景。用户下单场景返回“正在处理请勿重复提交”给前端而不是直接报错。后台任务场景跳过本轮下一轮再抢锁。数据迁移场景可以只允许管理员手动触发不依赖自动重试。10.4 锁粒度控制锁粒度越小并发能力越高。能锁商品 ID就不要锁整个商品分类能锁订单号就不要锁整个用户。但锁粒度太小也有问题比如同一用户同时提交两笔订单分别锁两个不同的订单号并不能防止用户重复提交。因此要结合业务需求来决定防重复提交锁用户级别或者用 RequestId。防超卖锁商品级别。防一单多领锁业务单据号。10.5 分布式锁不是银弹这点值得反复强调。分布式锁解决的是多实例并发互斥问题不解决数据一致性问题。一个完整的订单系统还需要MySQL / PostgreSQL 的库存字段使用乐观锁或行级锁兜底。唯一索引防止重复订单。消息队列做流量削峰和异步补偿。状态机防止订单状态乱跳。分布式锁只是其中一环。项目实战中一定要理解这一环的位置才能设计出真正高可用的架构。10.6 日志、监控和合规生产环境必须记录锁相关日志至少包含资源 key。token。加锁耗时。锁持有时间。释放锁是否成功。日志不要记录请求参数中的敏感信息。涉及用户隐私、支付信息的数据按安全合规要求脱敏处理。分布式锁服务本身也不能暴露到公网Redis 端口要限制访问来源。11. 总结与下一步这套 .NET 10 WebAPI 高并发架构实战最值得尝试的点就是“用一把锁把三类问题一次性串起来”。先建 WebAPI再接入 Redis然后在订单接口里用分布式锁解决超卖用幂等标记解决重复请求最后在后台任务里加锁解决多实例重复执行。整体链路短、代码量不大但涉及的知识点非常密集。最先应该验证的功能是下单接口的并发测试。把库存初始化为 5启动 20 个并发请求观察是不是只有 5 个成功。这个结果一出来你对分布式锁的“互斥”效果就有了最直观的体感。最容易踩的坑有两个。第一是锁过期时间设置得太短临界区没执行完锁就没了。第二是释放锁时不校验 token直接把别人的锁删了。写代码时盯着这两点基本不会出现大的并发事故。后续可以继续扩展的方向也很清晰把库存扣减从分布式锁方案换成 Lua 脚本方案做一轮性能对比。接入 RedLock模拟 Redis 主节点宕机观察锁的可靠性。增加数据库兜底用乐观锁和唯一索引做最后一道防线。引入压测工具把测试数据做成自动化脚本。在 Kubernetes 或 Docker Compose 里部署多个 WebAPI 实例验证真实集群环境下的锁表现。分布式锁是高并发架构的入门关卡但不是终点。把这套代码跑通之后再往上走就要考虑消息队列削峰、缓存穿透击穿雪崩、微服务事务一致性这些问题了。每一步都需要在实际项目中踩过坑才能形成真正的判断力。先动手把分布式锁落在业务上其他问题会随着场景自然展开。