高并发下的接口请求重复提交问题 在.Net开发中,我们经常遇到用户疯狂点击同一按钮,或者服务响应慢时重复发送请求,导致数据重复添加或混乱。这不仅浪费资源,更会得到错误的业务结果。如何高效解决这一普遍问题呢?
常规方案使用分布式锁 面对这问题,分布式锁是一种有效的传统解决方案,可以确保同一时间只有一个请求被处理。但面对众多需要锁定的接口,配置分布式锁无疑是一项繁重的工作。如何优化这一流程?
今天,我带来了一种简洁高效的方案。透过.Net中间件的强大功能,我们可以用一行代码轻松实现防并发。首先,我们定义一个特性ApiLock,并在中间件中实现基于用户或Token的Redis锁定。如此设计,简单实用又易于扩展。
首先,我们需要创建一个ApiLock得特性,用于判断哪些接口需要执行分布式锁
public class ApiLockAttribute : ValidationAttribute{public ApiLockAttribute(int maxLockTime = 10, string msg = "正在处理,请稍等,请勿重复提交"){MaxLockTime = maxLockTime;Msg = msg;}public int MaxLockTime { get; set; }public string Msg { get; set; }}然后我们需要写一个中间件,如果不了解中间件的小伙伴可以查看下面文章进行学习:
https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0

我们需要创建一个中间件:
public class ApiLockMiddleware : MiddlewareBase{public override async Task Invoke(HttpContext context){}}
然后我们需要再这个中间件里写一写逻辑,我需要通过HttpContext 获取到Token(用户或者客户端),来进行唯一标识的判定。
public class ApiLockMiddleware : MiddlewareBase{public override async Task Invoke(HttpContext context){//获取请求路由string url= context.Request.Path.Value.ToLower();}}
然后我们需要编写一个获取Endpoint的方法:
private static Endpoint GetEndpoint(HttpContext context){if (context == null){throw new ArgumentNullException(nameof(context));}return context.Features.Get<IEndpointFeature>()?.Endpoint;}
这个方法用于获取请求的EndPoint来判断是否包含ApiLock的特性
public class ApiLockMiddleware : MiddlewareBase{public override async Task Invoke(HttpContext context){//获取请求路由string url= context.Request.Path.Value.ToLower();var endpoint = GetEndpoint(context);if (endpoint != null){var apiLock = endpoint.Metadata.GetMetadata<ApiLockAttribute>();if (apiLock == null){//没有特性直接走await base.Invoke(context);return;}else{//这里才是我们要写 核心逻辑。我们需要获取token,//然后拼接token和url进行锁定using (var scope = _scopeFactory.CreateScope()){var redisLock = scope.ServiceProvider.GetRequiredService<IRedisLock>();var expiry = TimeSpan.FromSeconds(apiLock .MaxLockTime);//超时时间,如果内部执行超过expity则会释放锁var wait = TimeSpan.FromSeconds(3);//获取锁的时候等待的时间var retry = TimeSpan.FromSeconds(1);//每隔多少时间请求一次string key = $"ApiLock:{用户/客户端Token}:{url}";//锁的key 用户唯一ID+API路由作为锁条件,同一个接口没执行完前不允许执行下一次using (var redLock = await redisLock.CreateLockAsync(key, expiry, wait, retry)){if (!redLock.IsAcquired){//如果被锁定,则返回特性传入的失败消息await HandleExceptionAsync(context, new Exception(apiLock.Msg), (int)HttpStatusCode.OK);return;}else{//没有锁定才继续往后走Controller等业务逻辑await base.Invoke(context);return;}}}}}}}
这里我们的中间件就写完了。我们需要写一个注册的方法:
public static class ApiLockExtensions{/// <summary>/// 防止重复提交中间件/// </summary>/// <param name="builder"></param>/// <returns></returns>public static IApplicationBuilder UseApiLock(this IApplicationBuilder builder){if (builder == null){throw new ArgumentNullException(nameof(builder));}return builder.UseMiddleware<ApiLockMiddleware>();}}
然后,我们需要再Configure里进行注册:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider){//……app.UseApiLock();}
到这里我们的封装就已经完成了,那么我们改如何使用它呢
[ApiController][Route("api/[controller]/[action]")]public class TestController : ControllerBase{[HttpPost][ApiLock(10,"接口被锁定,请稍后再试")]public async Task<IActionResult> TestApiLock(){await Task.Delay(20000);return Ok();}}
这里也非常简单,我们直接再需要使用锁定的接口上添加ApiLock的特性就可以啦,我再这里对中间件提供了2个参数,分别是锁定的最大时间和锁定后的错误提示。这个大家也可以按照自身业务需求来进行扩展。
然后我们测试一下这个接口,这个接口里面做了20秒的延迟

我们可以看到,当我们连续点击2次测试接口时,我们发现第二次调用就会返回被锁定了。
简洁之美,效率之王 这不仅是一种技术优化,更是一种产品哲学的体现。在追求高效的同时,我们更希望能让开发者从重复的工作中解放出来,将更多的精力投入到创新和业务的核心中去。
即刻行动起来,用最简洁的代码,解决.Net API的高并发头疼问题吧!