ASP.NET Core使用filter和redis实现接口防重
背景
日常开发中,经常需要对一些响应不是很快的关键业务接口增加防重功能,即短时间内收到的多个相同的请求,只处理一个,其余不处理,避免产生脏数据。这和幂等性(idempotency)稍微有点区别,幂等性要求的是对重复请求有相同的效果和结果,通常需要在接口内部执行业务操作前检查状态;而防重可以认为是一个业务无关的通用功能,在ASP.NET Core中我们可以借助Filter和redis实现。
关于Filter
Filter的由来可以追溯到ASP.NET MVC中的ActionFilter和ASP.NET Web API中的ActionFilterAttribute。ASP.NET Core将这些不同类型的Filter统一为一种类型,称为Filter,以简化API和提高灵活性。ASP.NET Core中Filter可以用于实现例如身份验证、日志记录、异常处理、性能监控等各种功能。
通过使用Filter,我们可以在请求处理管道的特定阶段之前或者之后运行自定义代码,达到AOP的效果。
编码实现
防重组件的思路很简单,将第一次请求的某些参数作为标识符存入redis中,并设置过期时间,下次请求过来,先检查redis相同的请求是否已被处理;
作为一个通用组件,我们需要能让使用者自定义作为标识符的字段以及过期时间,下面开始实现。
PreventDuplicateRequestsActionFilter
public class PreventDuplicateRequestsActionFilter : IAsyncActionFilter { public string[] FactorNames { get; set; } public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } private readonly IDatabase _database; private readonly ILogger<PreventDuplicateRequestsActionFilter> _logger; public PreventDuplicateRequestsActionFilter(IConnectionMultiplexer redis, ILogger<PreventDuplicateRequestsActionFilter> logger) { _database = redis.GetDatabase(); _logger = logger; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var factorValues = new string?[FactorNames.Length]; var isFromBody = context.ActionDescriptor.Parameters.Any(r => r.BindingInfo?.BindingSource == BindingSource.Body); if (isFromBody) { var parameterValue = context.ActionArguments.FirstOrDefault().Value; factorValues = FactorNames.Select(name => parameterValue?.GetType().GetProperty(name)?.GetValue(parameterValue)?.ToString()).ToArray(); } else { for (var index = 0; index < FactorNames.Length; index++) { if (context.ActionArguments.TryGetValue(FactorNames[index], out var factorValue)) { factorValues[index] = factorValue?.ToString(); } } } if (factorValues.All(string.IsNullOrEmpty)) { _logger.LogWarning("Please config FactorNames."); await next(); return; } var idempotentKey = $"{context.HttpContext.Request.Path.Value}:{string.Join("-", factorValues)}"; var success = await _database.StringSetAsync(idempotentKey, DateTimeOffset.UtcNow.ToString(), AbsoluteExpirationRelativeToNow, When.NotExists); if (success) { await next(); } else { _logger.LogWarning("Received duplicate request({}), short-circuiting...", idempotentKey); context.Result = new AcceptedResult(); } } }
注意这里直接使用StringSetAsync
方法并设置When.NotExists
选项,这是为了避免高并发情况下数据不一致,When
对应的是redis命令里SET
里的NX
选项 -- 只有key不存在的时候才会设置。
PreventDuplicateRequestsActionFilter里,我们首先通过反射从 ActionArguments
拿到指定参数字段的值,由于从request body取值略有不同,我们需要分开处理;接下来开始拼接key并检查redis,如果key已经存在,我们需要短路请求,这里直接返回的是 Accepted (202)
而不是Conflict (409)
或者其它错误状态,是为了避免上游已经调用失败而继续重试。
PreventDuplicateRequestsAttribute
防重组件的全部逻辑在PreventDuplicateRequestsActionFilter
中已经实现,由于它需要注入 IDistributedCache
和ILogger
对象,我们使用IFilterFactory
实现一个自定义属性,方便使用。
[AttributeUsage(AttributeTargets.Method)] public class PreventDuplicateRequestsAttribute : Attribute, IFilterFactory { private readonly string[] _factorNames; private readonly int _expiredMinutes; public PreventDuplicateRequestsAttribute(int expiredMinutes, params string[] factorNames) { _expiredMinutes = expiredMinutes; _factorNames = factorNames; } public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) { var filter = serviceProvider.GetService<PreventDuplicateRequestsActionFilter>(); filter.FactorNames = _factorNames; filter.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expiredMinutes); return filter; } public bool IsReusable => false; }
注册
使用StackExchange.Redis操作redis;注册PreventDuplicateRequestsActionFilter
,PreventDuplicateRequestsAttribute
无需注册。
var multiplexer = ConnectionMultiplexer.Connect("localhost"); builder.Services.AddSingleton<IConnectionMultiplexer>(multiplexer); builder.Services.AddScoped<PreventDuplicateRequestsActionFilter>();
使用
假设我们有一个接口CancelOrder
,我们指定入参中的OrderId和Reason为因子。
namespace PreventDuplicateRequestDemo.Controllers { [Route("api/[controller]")] [ApiController] public class OrderController : ControllerBase { [HttpPost(nameof(CancelOrder))] [PreventDuplicateRequests(5, "OrderId", "Reason")] public async Task<IActionResult> CancelOrder([FromBody] CancelOrderRequest request) { await Task.Delay(1000); return new OkResult(); } } public class CancelOrderRequest { public Guid OrderId { get; set; } public string Reason { get; set; } } }
启动程序,多次调用api,除第一次调用成功,其余请求皆被短路
查看redis,已有记录
参考链接
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-7.0
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-7.0
作者: 马行空的博客
出处: https://www.cnblogs.com/netry/p/aspnetcore-prevent-duplicate-requests-filter-redis.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示