Loading

.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();  
    }
}

 

posted @ 2026-08-27 10:58  jevan  阅读(4)  评论(0)    收藏  举报