.NET Core API 中实现高效防颤处理,防重复请求实践
在实际项目中,一个 API 被短时间内重复调用并不少见。比如用户连续点击“提交订单”,前端可能瞬间发送多个请求;搜索框快速输入时,每个字符都可能触发一次接口;移动端网络抖动时,同一个请求甚至可能被客户端自动重试。如果 API 每收到一次请求都直接执行业务逻辑,那么问题就来了:
1、数据库可能被重复写入
2、相同任务可能被重复创建
3、第三方接口可能被重复调用
4、CPU 和数据库连接池产生额外压力
5、用户看到的结果可能与预期不一致
很多开发者第一反应是:
“加一个 500ms 防抖不就行了吗?”
因此,防止重复请求并不是简单地在接口前面加一个500ms判断,而是需要根据业务场景选择合适的策略。
一、一个按钮为什么可能产生多个 API 请求?
因此:防止重复请求不能只依赖按钮的 disabled,也不能只依赖前端 Debounce。
前端解决的是“尽量少发请求”,后端解决的则应该是:即使请求真的重复到达,业务也不能因此产生错误结果。
二、先搞清楚:防抖、节流、限流、幂等有什么区别?
| 方案 | 核心目的 | 典型场景 |
| Debounce 防抖 | 连续触发,只处理最后一次 | 搜索框输入 |
| Throttle 节流 | 限制单位时间内请求次数 | 高频查询、按钮点击 |
| 幂等 | 同一个业务请求只产生一次业务效果 | 订单、支付、创建任务 |
| Rate Limit 限流 | 限制整体请求速率 | API 防刷 |
| 锁 | 保证并发情况下同一资源只能被一个线程处理 | 库存、状态修改 |
三、实现示例
1、前端防抖Debounce
function debounce(fn, delay) { let timer; return function (args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } const search = debounce(async function (keyword) { const response = await fetch( '/api/search?keyword=${encodeURIComponent(keyword)}' ); const data = await response.json(); console.log(data); }, 500);
这样用户连续输入:
a
ap
app
appl
apple
前面的定时器不断被取消,最终只发送:
apple
这样可以明显减少 API 请求次数。
注意:它不能作为后端唯一的保护措施。原因:1、客户端代码可以被绕过。2、网络重试不受前端防抖控制(比较少见)。3、多个客户端(如开多个浏览器)可以同时调用
2、接口API Throttle 节流
netcore简单示例
//使用IMemoryCache保存最近一次请求时间 public class RequestThrottleService { private readonly IMemoryCache _cache; public RequestThrottleService(IMemoryCache cache) { _cache = cache; } public bool Allow(string key, TimeSpan interval) { var now = DateTime.UtcNow; if (_cache.TryGetValue(key, out DateTime lastTime)) { if (now - lastTime < interval) { return false; } } _cache.Set(key, now, interval); return true; } } Controller: private RequestThrottleService _throttle; // 等待依赖构造器注入 [HttpGet("status")] public IActionResult GetStatus() { string key = $ "status:{User.Identity?.Name}"; if (!_throttle.Allow( key, TimeSpan.FromSeconds(1))) { return StatusCode(429, new { message = "请求过于频繁" }); } return Ok(new { status = "OK" }); }
升级可使用中间件处理
public class RequestThrottleMiddleware { private readonly RequestDelegate _next; private static readonly ConcurrentDictionary < string, long > _requestTimes = new(); private static readonly TimeSpan _interval = TimeSpan.FromMilliseconds(500); public RequestThrottleMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { string key = BuildRequestKey(context); long now = Environment.TickCount64; if (_requestTimes.TryGetValue(key, out long lastTime)) { if (now - lastTime < _interval.TotalMilliseconds) { context.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.Response.WriteAsJsonAsync(new { code = 429, message = "请求过于频繁,请稍后再试" }); return; } } _requestTimes[key] = now; await _next(context); } private static string BuildRequestKey(HttpContext context) { string ip = context.Connection.RemoteIpAddress ? .ToString() ? ? "unknown"; return $ "{ip}:{context.Request.Path}"; } }
3、Rate Limit:限流(一个客户端在单位时间内最多允许发送多少请求。)
ASP.NET Core 可以使用内置 Rate Limiting Middleware。
例如:
builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter( "api", limiterOptions => { limiterOptions.PermitLimit = 100; limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.QueueLimit = 0; }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; });
然后启用:
var app = builder.Build(); app.UseRateLimiter(); app.MapGet("/api/data", () => { return Results.Ok(new { message = "success" }); }).RequireRateLimiting("api"); app.Run();
Rate Limit 特别适合:
- 登录接口
- 短信接口
- 查询接口
- 公共 API
- 防止恶意刷接口
但是它解决的是:
请求数量过多
而不是:
同一个订单被重复创建。
4、幂等
500ms 防重复解决的是“短时间重复请求”,Idempotency-Key 解决的是“同一个业务请求不能产生多个业务结果”。
public async Task < IActionResult > CreateOrder( [FromHeader(Name = "Idempotency-Key")] string requestId) { if (string.IsNullOrWhiteSpace(requestId)) { return BadRequest("缺少 Idempotency-Key"); } string key = $ "idempotency:order:{requestId}"; var cachedResult = await _redis.StringGetAsync(key); if (cachedResult.HasValue) { return Content( cachedResult.ToString(), "application/json"); } // 执行业务逻辑 var order = await _orderService.CreateAsync(); var result = JsonSerializer.Serialize(new { success = true, orderId = order.Id }); await _redis.StringSetAsync( key, result, TimeSpan.FromMinutes(10)); return Content(result, "application/json"); }
5、锁:只能解决单体服务并发问题
private static readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); [HttpPost("buy")] public async Task < IActionResult > Buy() { await _lock.WaitAsync(); try { var stock = await _stockService.GetStockAsync(); if (stock <= 0) { return BadRequest("库存不足"); } await _stockService.DecreaseAsync(); return Ok("购买成功"); } finally { _lock.Release(); } }
本文来自博客园,作者:jevan,转载请注明原文链接:https://www.cnblogs.com/DoNetCShap/p/22712766

浙公网安备 33010602011771号