.NET 10 WebAPI 集成 Redis 分布式锁:解决高并发秒杀与重复提交问题
各位读者朋友大家好。在过去的几年里我一直在参与电商、活动运营类后端系统的开发。随着业务量增长系统从单机部署演进到多节点集群随之而来的并发问题开始集中爆发秒杀场景库存被超卖、用户重复点击下单生成了多笔订单、多个服务节点同时操作同一条数据导致状态错乱。这些问题的本质是多个进程在没有协调机制的情况下并发访问了共享资源。在 .NET 技术栈中过去我们最常用的方案是lock关键字或Monitor但这类方案只能在单个进程内生效。集群环境下我们需要一把跨进程的锁也就是分布式锁。Redis 作为高性能的键值存储中间件是解决这个问题的首选武器。而随着 .NET 10 预览版的发布配合新一代 WebAPI 项目模板与 AI 辅助开发工具我们完全可以在更短的时间内搭建一套具备高并发处理能力的后端服务。本文将围绕这套架构展开完整实战从 Redis 环境准备到 .NET 10 WebAPI 项目创建从封装 Redis 基础服务到实现基于分布式锁的秒杀与幂等性接口最后会给出常见问题排查清单与工程化建议。文章中的代码都经过整理可以直接复制到你的项目中运行。如果你正准备学习高并发系统设计或者正在为自己的 .NET 项目寻找一套可落地的分布式锁解决方案这篇文章值得你耐心读完。1. 高并发场景与分布式锁核心概念1.1 高并发下最棘手的三个问题先看三个最常见的业务痛点超卖问题秒杀活动中库存只有 10 件但最终成交了 15 笔订单。原因就是并发请求同时读到库存为 1同时执行了扣减。重复请求前端提交订单时用户因为网络原因连续点了两次提交按钮。两次请求落到后端集群的不同节点导致生成了两笔订单。数据竞争多个服务实例同时对数据库中的同一行记录执行读取-修改-写回操作后提交的操作覆盖了先提交的结果。这三个问题在高并发系统中非常典型。在单机时代我们可以用lock锁住当前进程内的代码块确保同一时刻只有一个线程执行关键逻辑。但在集群部署模式下请求会通过负载均衡分发到不同的服务器节点lock无法跨进程生效。1.2 分布式锁的核心思想分布式锁的目标是让分布在多台机器上的多个进程在访问共享资源时实现互斥。它的工作流程一般是某个进程在访问共享资源前尝试获取锁。如果获取成功执行关键业务逻辑。执行完毕后主动释放锁。如果其他进程尝试获取锁发现锁已被持有则等待或直接返回失败。Redis 实现分布式锁主要依赖它的原子命令。比如SET key value NX EX seconds这个命令可以保证只有当 key 不存在时才设置键值这正好满足了互斥的要求同时还能设置过期时间避免进程崩溃导致锁无法释放。与数据库悲观锁、ZooKeeper 分布式锁相比Redis 分布式锁的优势在于性能高、实现简单、应用广泛绝大多数后端项目都能快速集成。当然它也有局限性比如锁的可靠性依赖 Redis 主从同步的最终一致性在极端故障场景下可能出现两个进程同时获得锁的情况。对于大多数非金融级业务场景Redis 分布式锁已经足够。1.3 AI 辅助开发在 .NET 项目中怎么用这两年 AI 辅助开发的普及速度非常快。在 .NET 生态中我们可以借助 GitHub Copilot、通义灵码、CodeGeeX 等工具加速编码也可以利用 ChatGPT、Claude 等大模型软件生成核心代码片段、补充单元测试、分析异常堆栈。本文的实战案例将按照需求描述 → AI 生成核心代码 → 人工审查与调整 → 压测验证的流程展开。我会在关键步骤说明哪些代码可以由 AI 生成哪些部分必须人工把关。 AI 工具能帮我们节省大量 CRUD 和格式代码的编写时间但分布式锁的原子性要求、Redis 命令的语义边界仍然需要开发者自己理解清楚。2. 环境准备与版本说明在开始写代码之前我们需要准备好开发环境。这里我列出本文使用的环境具体版本你可以根据自己的实际情况调整。环境组件版本/说明操作系统Windows 11 / Windows Server 2022也兼容 macOS、LinuxIDEVisual Studio 2022 或 Visual Studio 2024需安装 ASP.NET 和 Web 开发工作负载SDK.NET 10 Preview SDKRedisRedis 7.x可通过 Windows 安装包、Docker 或 Linux 源码安装数据库本文示例使用内存存储模拟商品库存生产环境建议使用 SQL Server / MySQL / PostgreSQL版本控制Git需要特别说明的是.NET 10 目前处于预览阶段本文的核心知识点——WebAPI 项目结构、Redis 分布式锁实现方式——在 .NET 6、.NET 8、.NET 9 中间同样适用。如果你使用的是长期支持版本 .NET 8代码完全可以直接复用。版本差异主要集中在项目文件格式和部分 API 命名空间上后面遇到时我会提醒你。Redis 的部署方式比较多我建议你优先选择 Dockerdocker run --name redis7 -p 6379:6379 -d redis:7.0如果是 Windows 本机学习也可以下载 Redis 的 Windows 版本压缩包解压后直接运行redis-server.exe redis.windows.conf安装完成后可以用 ping 命令验证 Redis 是否正常工作redis-cli ping如果返回PONG说明 Redis 服务已正常运行。后续我们会在 .NET 项目中通过 NuGet 包连接 Redis。3. Redis 基础与 .NET 集成方案3.1 Redis 的核心数据结构和应用场景在进入分布式锁之前先快速梳理 Redis 的几种常用数据类型。理解这些类型对我们后面选择缓存策略、设计锁的实现方式会有帮助。String最基础的类型可以存储字符串、整数、序列化后的对象。分布式锁的 key-value 就是典型的 String 应用。Hash适合存储对象字段比如用户信息。与 String 序列化整个对象相比Hash 可以单独读写某个字段。List适合做消息队列、最新消息列表。比如记录用户的操作日志。Set自动去重的集合适合做标签、关注关系。ZSet有序集合可以按分数排序适合排行榜。在 .NET 中我们需要一个 Redis 客户端来执行这些命令。目前最常用的是StackExchange.Redis它是一个高性能的官方推荐客户端支持异步方法、管道批量操作、连接多路复用等特性。另一款是NewLife.Redis国内社区使用较多封装较友好。本文以StackExchange.Redis为例。3.2 创建 .NET 10 WebAPI 项目打开 Visual Studio选择创建新项目选择 ASP.NET Core Web API 模板。如果你使用的是命令行工具也可以执行dotnet new webapi -n HighConcurrencyDemo然后进入项目目录添加 StackExchange.Redis 包cd HighConcurrencyDemo dotnet add package StackExchange.Redis项目创建完成后建议目录结构如下HighConcurrencyDemo/ ├── Controllers/ │ ├── ProductsController.cs │ ├── SeckillController.cs │ └── OrderController.cs ├── Services/ │ ├── RedisService.cs │ ├── DistributedLockService.cs │ ├── OrderService.cs │ └── ProductService.cs ├── Models/ │ ├── Product.cs │ └── Order.cs ├── appsettings.json └── Program.cs3.3 编写 Redis 连接配置在appsettings.json中添加 Redis 连接字符串{ Redis: { ConnectionString: 127.0.0.1:6379,password,defaultDatabase0,abortConnectfalse, InstanceName: HighConcurrency_ }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }这里对连接字符串的几个参数做说明abortConnectfalse当 Redis 服务暂时不可用时允许连接对象在后台重试而不是启动时直接抛异常。这对提升应用启动鲁棒性很有帮助。defaultDatabase0默认使用 DB 0。如果业务需要隔离可以按业务划分数据库编号。InstanceName作为 key 前缀避免多个项目共用 Redis 时出现 key 冲突。然后我们编写一个RedisService用来统一封装获取数据库对象的逻辑。这里不做过度的二次封装只做最基础的连接管理。// 文件路径Services/RedisService.cs using StackExchange.Redis; namespace HighConcurrencyDemo.Services; public class RedisService { private readonly ConnectionMultiplexer _connection; private readonly string _instanceName; public RedisService(string connectionString, string instanceName) { _connection ConnectionMultiplexer.Connect(connectionString); _instanceName instanceName; } public IDatabase GetDatabase() { return _connection.GetDatabase(); } public string GetKey(string key) { return ${_instanceName}{key}; } }在Program.cs中注册为单例// 文件路径Program.cs using HighConcurrencyDemo.Services; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var redisConfig builder.Configuration.GetSection(Redis); var connectionString redisConfig[ConnectionString] ?? 127.0.0.1:6379; var instanceName redisConfig[InstanceName] ?? HighConcurrency_; builder.Services.AddSingleton(new RedisService(connectionString, instanceName)); builder.Services.AddScopedOrderService(); builder.Services.AddScopedProductService(); builder.Services.AddSingletonDistributedLockService(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();到这里项目的 Redis 基础连接就打通了。下一步我们来设计分布式锁的服务封装。4. 分布式锁的设计与实现4.1 基于 SET NX EX 命令实现锁原语Redis 官方推荐的分布式锁最简实现是使用SET key value NX EX命令。这个命令的含义是如果 key 不存在则设置 key 的值为 value并指定过期时间如果 key 已存在则不做任何操作。执行结果返回OK表示获取锁成功返回(nil)表示获取失败。StackExchange.Redis 为我们封装了LockTake和LockRelease方法底层使用的就是这套命令// 获取锁 bool lockTaken db.LockTake(lockKey, lockValue, TimeSpan.FromSeconds(10)); // 释放锁 bool lockReleased db.LockRelease(lockKey, lockValue);注意释放锁时必须传入锁值lockValue。为什么需要这个值考虑一个场景进程 A 获取锁后因为 GC 停顿或网络阻塞导致锁超时自动过期了。这时进程 B 获取到了锁并开始执行。如果 A 在超时之后继续执行完成并调用LockRelease但因为我们传入了 A 的锁值而 Redis 在释放时会校验 value 是否一致所以 B 的锁不会被 A 误删。这个逻辑非常重要它是分布式锁安全性的第一道防线。团队里不少新手在实现分布式锁时直接用KeyExists判断是否存在然后执行KeyDelete这种写法在高并发下是存在隐患的一定要避免。4.2 封装 DistributedLockService在真实项目中我们不会在每个 Controller 里直接写LockTake和LockRelease。更好的方式是把获取锁、执行方法、释放锁、异常处理串成一个标准流程封装成工具方法。// 文件路径Services/DistributedLockService.cs using StackExchange.Redis; using HighConcurrencyDemo.Services; namespace HighConcurrencyDemo.Services; public class DistributedLockService { private readonly RedisService _redisService; private readonly ILoggerDistributedLockService _logger; public DistributedLockService(RedisService redisService, ILoggerDistributedLockService logger) { _redisService redisService; _logger logger; } /// summary /// 执行一个受分布式锁保护的方法。 /// /summary /// param namelockKey锁的 key建议与业务资源相关/param /// param namelockValue锁的 value用于安全释放建议使用请求唯一标识/param /// param nameexpiry锁自动过期时间/param /// param nameaction需要执行的业务方法/param /// param namewaitTime获取锁的最长等待时间超过则返回 false/param public bool TryExecuteWithLock(string lockKey, string lockValue, TimeSpan expiry, Action action, TimeSpan? waitTime null) { var db _redisService.GetDatabase(); var fullKey _redisService.GetKey(lockKey); var deadline DateTime.UtcNow (waitTime ?? TimeSpan.FromSeconds(5)); while (DateTime.UtcNow deadline) { if (db.LockTake(fullKey, lockValue, expiry)) { try { action(); return true; } catch (Exception ex) { _logger.LogError(ex, 执行加锁业务失败lockKey{LockKey}, lockKey); throw; } finally { db.LockRelease(fullKey, lockValue); } } Thread.Sleep(100); } return false; } }这个服务比直接在 Controller 中加锁更易用支持等待时间。如果锁暂时被其他人持有当前线程会在一个循环内重试而不是直接返回失败。使用finally保证锁一定会被释放避免业务异常导致锁永久不释放。将 value 与 lockKey 绑定释放锁时由 Redis 校验 value防止误删他人持有的锁。4.3 为什么说要慎用 RedLock 算法在 Redis 官方文档中还提供了一种名为 RedLock 的算法它要求锁在多个 Redis 节点上同时获取超过半数节点成功才算获取成功。RedLock 的初衷是解决 Redis 主从切换导致锁丢失的问题。但是RedLock 在分布式系统领域存在较大争议。著名分布式系统专家 Martin Kleppmann 撰写了长文批评 RedLock 的安全性Redis 作者 Salvatore Sanfilippo 也进行了回应。争论的核心在于锁的租约续期时钟跳跃GC 停顿等细节。对于绝大多数中小团队的业务系统RedLock 的复杂度超出了实际需求。我更推荐的做法是业务上控制锁的粒度尽量缩短锁持有时间。合理设置过期时间避免业务执行时间超过锁时长。如果对安全性有更高要求可以引入看门狗机制自动续期或者升级为 ZooKeeper 分布式锁。在实际项目中不要盲目迷信算法越复杂越安全。理解业务场景对一致性的要求比套用复杂算法更重要。5. 实战秒杀接口 分布式锁 幂等处理下面我们进入实战阶段。我会带大家完成三个核心接口一个受分布式锁保护的商品秒杀接口。一个配合 Lua 脚本实现原子扣减库存的完整方案。一个防止用户重复提交订单的幂等性接口。这三个接口涵盖了高并发场景最常见的设计思路。5.1 商品模型与内存库存为了避免引入数据库环境依赖本文先用一个静态内存字典模拟商品库存。生产环境中你只需要把同样的逻辑换成 SQL 更新语句或存储过程即可。// 文件路径Models/Product.cs namespace HighConcurrencyDemo.Models; public class Product { public int Id { get; set; } public string Name { get; set; } string.Empty; public int Stock { get; set; } public decimal Price { get; set; } }// 文件路径Services/ProductService.cs using HighConcurrencyDemo.Models; namespace HighConcurrencyDemo.Services; public class ProductService { private static readonly Dictionaryint, Product _products new() { { 1, new Product { Id 1, Name 高性能编程实战课, Stock 100, Price 0.01M } }, { 2, new Product { Id 2, Name Redis 分布式锁专题课, Stock 50, Price 0.01M } } }; private readonly RedisService _redisService; public ProductService(RedisService redisService) { _redisService redisService; } public int GetStock(int productId) { if (_products.TryGetValue(productId, out var product)) { return product.Stock; } return 0; } public bool TryReduceStock(int productId, int quantity) { var product _products[productId]; if (product.Stock quantity) { return false; } product.Stock - quantity; return true; } public void ResetStock(int productId, int stock) { if (_products.ContainsKey(productId)) { _products[productId].Stock stock; } } }ProductService的TryReduceStock看起来是正确的先判断库存是否充足再减少库存。但在多线程环境下这段代码存在严重的线程安全问题。多个线程可能同时读取到Stock 1都通过了 if 判断然后依次执行product.Stock - quantity。最终库存变成负数。这就是我们要处理的核心问题。5.2 秒杀接口使用分布式锁保护库存扣减先看第一种实现方案用我们封装的TryExecuteWithLock来保护整个查询库存-扣减库存的流程。// 文件路径Controllers/SeckillController.cs using HighConcurrencyDemo.Services; using Microsoft.AspNetCore.Mvc; namespace HighConcurrencyDemo.Controllers; [ApiController] [Route(api/[controller])] public class SeckillController : ControllerBase { private readonly ProductService _productService; private readonly DistributedLockService _lockService; public SeckillController(ProductService productService, DistributedLockService lockService) { _productService productService; _lockService lockService; } [HttpPost(buy/{productId})] public IActionResult Buy(int productId, [FromQuery] int userId, [FromQuery] int quantity 1) { // 业务操作必须携带用户标识便于幂等控制和审计 if (userId 0) { return BadRequest(new { code 400, message 用户标识不能为空 }); } var lockKey $seckill:product:{productId}; var lockValue ${userId}_{Guid.NewGuid():N}; var isSuccess _lockService.TryExecuteWithLock( lockKey, lockValue, TimeSpan.FromSeconds(10), () { var stock _productService.GetStock(productId); if (stock quantity) { throw new InvalidOperationException(库存不足); } // 模拟耗时操作订单创建、支付单生成等 Thread.Sleep(50); _productService.TryReduceStock(productId, quantity); }, TimeSpan.FromSeconds(3)); if (!isSuccess) { return StatusCode(429, new { code 429, message 系统繁忙请稍后重试 }); } return Ok(new { code 200, message 抢购成功 }); } }这段代码有几个关键设计锁的粒度是seckill:product:{productId}也就是说不同的商品可以使用不同的锁互不干扰。如果只用一把全局锁那么所有商品的秒杀请求都会被串行化性能会很差。锁的 value 使用userId Guid释放锁时由 Redis 校验避免线程间误删。等待时间为 3 秒如果 3 秒内拿不到锁直接返回 429提示用户系统繁忙。这比无限等待更适合秒杀类场景。5.3 进阶优化使用 Lua 脚本保证原子性上面的实现虽然正确但每次秒杀都要获取锁 → 执行业务 → 释放锁中间还有Thread.Sleep(50)这样的耗时操作在高并发下锁的排队时间会比较长。有没有更高效的方式我们可以把检查库存 扣减库存封装成一段 Lua 脚本在 Redis 服务端原子执行。Redis 是单线程执行命令的Lua 脚本的执行不会被其他命令打断。这样一来我们甚至可以不使用分布式锁直接在 Redis 中完成库存扣减然后异步将订单落库。-- 文件路径Scripts/StockDeduct.lua -- KEYS[1] 商品库存 key -- ARGV[1] 扣减数量 local stock tonumber(redis.call(GET, KEYS[1]) or 0) if stock tonumber(ARGV[1]) then redis.call(DECRBY, KEYS[1], ARGV[1]) return 1 end return 0下面我们改造ProductService增加一个基于 Redis 库存扣减的方法。// 文件路径Services/ProductService.cs 追加方法 public async Taskbool TryReduceStockWithLuaAsync(int productId, int quantity) { var db _redisService.GetDatabase(); var stockKey _redisService.GetKey($product:stock:{productId}); // 假设初始化库存时会将库存写入 Redis // 这里模拟 Lua 脚本 var script local stock tonumber(redis.call(GET, KEYS[1]) or 0) if stock tonumber(ARGV[1]) then redis.call(DECRBY, KEYS[1], ARGV[1]) return 1 end return 0; var result await db.ScriptEvaluateAsync( script, new RedisKey[] { stockKey }, new RedisValue[] { quantity }); return (int)result 1; }调用方可以去掉分布式锁直接执行扣减var isSuccess await _productService.TryReduceStockWithLuaAsync(productId, quantity); if (!isSuccess) { return BadRequest(new { code 400, message 库存不足 }); }Lua 脚本方案的优点很明显减少了客户端与 Redis 之间的网络往返次数同时 Redis 的原子性天然解决了并发问题。它的适用场景是库存作为唯一事实源的秒杀场景。但需要注意如果库存数据最终需要与数据库保持一致你还需要处理 Redis 与数据库之间的同步问题通常的做法是先扣 Redis 库存再异步写数据库订单最后对账。5.4 防重复提交基于分布式锁的幂等接口除了超卖问题用户重复点击导致的重复请求也很常见。解决思路有两种前端在提交按钮点击后立刻置灰这是第一层防护。后端必须提供幂等能力即同一个请求 ID 只能处理一次。我们以后端实现为例。核心思路在执行业务前先尝试把请求 ID 作为 key 写入 Redis设置过期时间为 5 分钟。如果写入成功说明这是一个新请求可以继续处理如果写入失败说明这个请求已经处理过了直接返回之前的结果。但这里存在一个问题如果业务处理时间很短但后续还有一个异步流程需要处理那么请求完成后我们应该删除这个 key 吗不一定。如果业务处理完成后立即删除 key那么用户稍后用一个相同的请求 ID 再次调用系统会认为是新请求幂等保护就失效了。更好的方案是让 key 保留一段时间在这段时间内重复请求都会被拒绝。实现代码如下// 文件路径Controllers/OrderController.cs using HighConcurrencyDemo.Services; using Microsoft.AspNetCore.Mvc; namespace HighConcurrencyDemo.Controllers; [ApiController] [Route(api/[controller])] public class OrderController : ControllerBase { private readonly RedisService _redisService; private readonly ILoggerOrderController _logger; public OrderController(RedisService redisService, ILoggerOrderController logger) { _redisService redisService; _logger logger; } [HttpPost(create)] public async TaskIActionResult CreateOrder([FromBody] CreateOrderRequest request) { if (string.IsNullOrWhiteSpace(request.RequestId)) { return BadRequest(new { code 400, message 请求 ID 不能为空 }); } var db _redisService.GetDatabase(); var idempotentKey _redisService.GetKey($idempotent:order:{request.RequestId}); // 尝试写入只有第一次调用才会返回 true bool isNewRequest await db.StringSetAsync(idempotentKey, 1, TimeSpan.FromMinutes(5), When.NotExists); if (!isNewRequest) { _logger.LogWarning(重复请求被拦截requestId{RequestId}, request.RequestId); return Ok(new { code 200, message 该请求已处理请勿重复提交, requestId request.RequestId }); } // TODO: 真正的下单逻辑 await Task.Delay(100); return Ok(new { code 200, message 下单成功, orderId Guid.NewGuid():N }); } }这里用到的是StringSetAsync的When.NotExists参数底层对应 Redis 的SET NX命令。这个方案的优点是不需要额外的锁对象一条命令就完成了幂等控制而且性能极高。使用这种方式时还要注意几个细节如果业务处理失败是否需要删除幂等 key我的建议是如果失败是业务逻辑失败比如库存不足可以删除 key让用户重试如果失败是系统异常比如数据库连接超时建议保留 key把失败原因记录到日志中。幂等 key 的过期时间要结合业务设置。对于下单业务5 到 10 分钟通常足够对于支付回调建议设置 24 小时或更长。6. 并发压测与结果验证代码写完之后我们需要验证分布式锁真的能解决超卖问题。这里我以 Apache JMeter 或 Postman 的 Runner 功能为例简单演示压测方法。我们设置一个商品ID 1库存 100。使用 200 个并发线程每个线程执行 1 次秒杀请求。如果分布式锁生效最终库存应该恰好为 0同时只有 100 个请求返回成功。在 Program.cs 的初始化中我们需要确保启动时把库存写入 Redis// 文件路径Program.cs 中的初始化逻辑 var app builder.Build(); using (var scope app.Services.CreateScope()) { var productService scope.ServiceProvider.GetRequiredServiceProductService(); var redisService scope.ServiceProvider.GetRequiredServiceRedisService(); var db redisService.GetDatabase(); // 初始化库存到 Redis db.StringSet(redisService.GetKey(product:stock:1), 100); db.StringSet(redisService.GetKey(product:stock:2), 50); } app.Run();压测完成后可以执行 Redis 命令检查剩余库存redis-cli GET HighConcurrency_product:stock:1预期输出0如果返回负数说明你的代码中仍然存在未受保护的并发写操作。这时候需要回到代码里检查是否所有查询-扣减流程都使用了分布式锁或 Lua 脚本。要注意的是如果使用分布式锁方案务必将锁的过期时间设置的比业务执行时间更长。否则业务还没执行完锁已经过期其他线程就能拿到新锁同样可能出现超卖。7. 常见问题与排查思路在配置和使用 Redis 分布式锁的过程中下面几个问题非常常见。我把它们整理成一张表方便你快速定位。问题现象常见原因解决思路连接 Redis 时报错Could not connect to RedisRedis 服务未启动或连接字符串配置错误先用redis-cli ping检查服务再检查 IP、端口、密码程序启动后大量超时异常连接字符串未设置abortConnectfalseRedis 短暂故障导致应用崩溃加入abortConnectfalse并在应用层增加重试策略两个线程同时拿到锁锁的 key 粒度太大或太小锁的 value 未校验锁过期时间设置过短检查锁 key 设计确认使用LockRelease(fullKey, value)而非直接删除 key业务执行成功但库存仍然超卖使用了lock关键字跨进程加锁或锁释放后库存扣减未保证原子性改用分布式锁或 Lua 脚本确认数据库扣减语句使用原子 SQL 如UPDATE ... SET Stock Stock - quantity WHERE Stock quantity秒杀接口响应太慢锁等待时间过长或业务临界区代码执行太久缩短临界区操作把非核心操作移出锁调整锁等待时间幂等接口误拦截正常请求请求 ID 重复或过期时间设置过短导致旧 key 未过期确认前端每个按钮点击生成唯一 RequestId评估业务处理时长合理设置过期时间Redis 内存持续增长缓存 key 没有设置过期时间或过期时间过长统一为缓存 key 设置 TTL定期清理无效 key如果你在项目里遇到了表中没覆盖到的问题建议先打开 StackExchange.Redis 的日志观察 Redis 命令的执行时间和失败详情。大多数分布式锁问题都集中在锁的原子性和锁的释放安全性这两个点上。8. 最佳实践与工程建议8.1 控制锁粒度缩短临界区分布式锁的本质是牺牲并发度来换取数据一致性。所以锁的粒度一定要小锁内的代码要尽量精简。不要把外部 HTTP 调用、文件上传、复杂计算等操作放在锁内否则系统吞吐量会急剧下降。正确的做法是锁内只做检查-更新这类必须保证原子性的操作。其他耗时的业务逻辑放在锁外执行。比如秒杀场景下锁内只更新库存而发送消息通知、生成订单详情这些操作可以放到消息队列中异步处理。8.2 为锁设置合理的过期时间Redis 分布式锁必须设置过期时间这是为了防止持有锁的进程崩溃导致死锁。过期时间不能过短否则业务还没执行完锁就自动释放了也不能过长否则 Redis 中会堆积大量无效锁 key。通常建议的初始值定为 5 到 15 秒。如果业务执行时间较长可以实现一个简单的看门狗续期逻辑起一个后台线程每隔三分之一过期时间就刷新一次锁的 TTL。网上有很多现成实现关键是要理解续期和释放之间存在竞态释放时仍然要校验 value。8.3 使用连接池避免频繁创建连接StackExchange.Redis的ConnectionMultiplexer是线程安全的推荐全局复用一个实例。不要在每个 Controller 或 Service 里 new 一个新的连接对象那会导致连接对象过多、端口被占用甚至触发 Redis 的连接数限制。我们在RedisService中已经把ConnectionMultiplexer注册为单例这一步是必要的。8.4 统一缓存 key 前缀与序列化方式多个服务共享同一个 Redis 实例时必须通过 key 前缀做隔离。我们前面在连接配置中设置了InstanceName所有 key 都自动加上前缀这是一个良好的习惯。另外Redis 存储对象时需要把对象序列化为 JSON 或二进制。建议统一使用System.Text.Json或Newtonsoft.Json并配置统一的命名策略。避免不同服务使用不同的序列化方式导致的数据解析异常。8.5 AI 辅助开发的正确打开方式最后说说 AI 辅助开发。在实际编码中我会让 AI 工具做这些事情根据接口需求生成 Controller 骨架代码。生成 Redis 服务的基本封装。为关键方法生成单元测试。解释某个 Redis 命令的底层行为。分析异常堆栈给出排查方向。但有一些事情我不会完全交给 AI锁的安全释放逻辑。Lua 脚本的原子性设计。幂等 key 的生命周期管理。分布式锁的降级和容错方案。原因很简单AI 工具擅长从已有的大量代码中学习模式但分布式系统的一致性设计需要结合实际业务场景。某个方案在 A 项目中是正确的在 B 项目中可能因为网络延迟、GC 行为不同而失效。理解原理AI 才能成为你的得力助手。在实际开发过程中应该将 AI 生成的代码视为初稿人工审查核心逻辑后再合入主线。对于涉及资金、订单、库存的系统尤其需要编写单元测试和进行并发压测来验证正确性。9. 总结与下一步学习方向本文从一个常见的业务痛点出发——高并发场景下的超卖、重复请求和集群并发冲突逐步展开了基于 .NET 10 WebAPI Redis 的分布式锁解决方案。核心内容可以概括为三点在集群环境下单机的lock关键字无法解决跨进程资源共享问题必须引入分布式锁。Redis 分布式锁基于SET NX EX命令实现释放锁时必须校验 value避免误删他人持有的锁。对于秒杀类高性能场景Lua 脚本提供的原子性操作比分布式锁更高效是更优的选择。你还应该掌握幂等接口的实现方式利用SET NX命令把请求 ID 写入 Redis相同请求在过期时间内被自动拦截。掌握了这些内容之后如果还想继续深入我建议你按下面的路线学习深入学习 Redis 持久化机制RDB 与 AOF 的优缺点以及它们对分布式锁可靠性影响。了解 Redisson 看门狗机制在 Java 中的实现并思考如何在 .NET 中实现类似功能。研究分布式事务与同步方案比如本地消息表、事务消息、Saga 事务模式。学习容器化部署如何使用 Docker Compose 同时编排 WebAPI 和 Redis 主从集群。实战演练用 JMeter 或 k6 对分布式锁接口进行压力测试分析吞吐量和响应时间曲线。高并发系统设计是一个需要不断实战验证的领域。我建议你在本地把本文的秒杀接口跑起来然后启动 200 个并发线程亲眼看一下库存数据的变化。只有当你能解释清楚锁在每一个瞬间的状态才算是真正掌握了分布式锁。如果这篇文章对你有帮助记得收藏备用。也欢迎在评论区分享你在分布式锁使用过程中遇到的问题我会在后续的文章中继续补充。下一篇文章我会结合一个具体的分布式锁续期案例和大家聊聊 Redisson 看门狗机制在 .NET 中的实现思路。感谢阅读我们下一篇见。