高并发下的接口请求重复提交问题 在.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的高并发头疼问题吧!