|
背景
日常开发中,经常需要对一些响应不是很快的关键业务接口增加防重功能,即短时间内收到的多个相同的请求,只处理一个,其余不处理,避免产生脏数据。这和幂等性(idempotency)稍微有点区别,幂等性要求的是对重复请求有相同的效果和结果,通常需要在接口内部执行业务操作前检查状态;而防重可以认为是一个业务无关的通用功能,在http://ASP.NET Core中我们可以借助Filter和redis实现。
关于Filter
Filter的由来可以追溯到http://ASP.NET MVC中的ActionFilter和http://ASP.NET Web API中的ActionFilterAttribute。http://ASP.NET Core将这些不同类型的Filter统一为一种类型,称为Filter,以简化API和提高灵活性。http://ASP.NET Core中Filter可以用于实现例如身份验证、日志记录、异常处理、性能监控等各种功能。

通过使用Filter,我们可以在请求处理管道的特定阶段之前或者之后运行自定义代码,达到AOP的效果。

编码实现
防重组件的思路很简单,将第一次请求的某些参数作为标识符存入redis中,并设置过期时间,下次请求过来,先检查redis相同的请求是否已被处理;
作为一个通用组件,我们需要能让使用者自定义作为标识符的字段以及过期时间,下面开始实现。
临时加更干货分享
大家能看到这里,已是对我们的支持了。C#零基础教程
新手零基础强烈推荐 入门到精通全讲解 全新录制C#基础语法全套视频60P+代码 全套合集

资料免费自取:
由于内容过多不便呈现,需要视频教程和配套源码的小伙伴,点击下方卡片!
可点击下方卡片:点击后自动复制威芯号,并跳转到威芯。搜索威芯号添加,内容已做打包,备注知乎
即可免费领取,注意查收!
PreventDuplicateRequestsActionFilter
public class PreventDuplicateRequestsActionFilter : IAsyncActionFilter
{
public string[] FactorNames { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
private readonly IDistributedCache _cache;
private readonly ILogger<PreventDuplicateRequestsActionFilter> _logger;
public PreventDuplicateRequestsActionFilter(IDistributedCache cache, ILogger<PreventDuplicateRequestsActionFilter> logger)
{
_cache = cache;
_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(&#34;Please config FactorNames.&#34;);
await next();
return;
}
var idempotentKey = $&#34;{context.HttpContext.Request.Path.Value}:{string.Join(&#34;-&#34;, factorValues)}&#34;;
var idempotentValue = await _cache.GetStringAsync(idempotentKey);
if (idempotentValue != null)
{
_logger.LogWarning(&#34;Received duplicate request({},{}), short-circuiting...&#34;, idempotentKey, idempotentValue);
context.Result = new AcceptedResult();
}
else
{
await _cache.SetStringAsync(idempotentKey, DateTimeOffset.UtcNow.ToString(),
new DistributedCacheEntryOptions {AbsoluteExpirationRelativeToNow = AbsoluteExpirationRelativeToNow});
await next();
}
}
}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;
}注册
为了简单,操作redis,直接使用微软Microsoft.Extensions.Caching.StackExchangeRedis包;注册PreventDuplicateRequestsActionFilter,PreventDuplicateRequestsAttribute无需注册。
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = &#34;127.0.0.1:6379,DefaultDatabase=1&#34;;
});
builder.Services.AddScoped<PreventDuplicateRequestsActionFilter>();使用
假设我们有一个接口CancelOrder,我们指定入参中的OrderId和Reason为因子。
namespace PreventDuplicateRequestDemo.Controllers
{
[Route(&#34;api/[controller]&#34;)]
[ApiController]
public class OrderController : ControllerBase
{
[HttpPost(nameof(CancelOrder))]
[PreventDuplicateRequests(5, &#34;OrderId&#34;, &#34;Reason&#34;)]
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 |
|