• 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可以用于实现例如身份验证、日志记录、异常处理、性能监控等各种功能。

    image

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

    image

    编码实现

    防重组件的思路很简单,将第一次请求的某些参数作为标识符存入redis中,并设置过期时间,下次请求过来,先检查redis相同的请求是否已被处理;
    作为一个通用组件,我们需要能让使用者自定义作为标识符的字段以及过期时间,下面开始实现。

    PreventDuplicateRequestsActionFilter

    public class PreventDuplicateRequestsActionFilter : IAsyncActionFilter
    {
    public string[] FactorNames { get; set; }
    public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
    private readonly IDatabase _database;
    private readonly ILogger _logger;
    public PreventDuplicateRequestsActionFilter(IConnectionMultiplexer redis, ILogger 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中已经实现,由于它需要注入 IDistributedCacheILogger对象,我们使用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();
    filter.FactorNames = _factorNames;
    filter.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expiredMinutes);
    return filter;
    }
    public bool IsReusable => false;
    }

    注册

    使用StackExchange.Redis操作redis;注册PreventDuplicateRequestsActionFilterPreventDuplicateRequestsAttribute无需注册。

    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 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,除第一次调用成功,其余请求皆被短路
    image

    查看redis,已有记录
    image

    参考链接

    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

  • 相关阅读:
    `Promise`全面解析
    甲氧基PEG多巴胺DPA-mPEG,Dopamine-mPEG,PEG化的多巴胺具有良好的水溶性
    联邦学习综述三
    基于SPI机制手动模拟数据库驱动打成Jar包使用
    JVM——一些零散的概念(后续学习深入了再补充)
    反射_集合,,220817,,
    使用ffmepg实现多路视频流合并
    python交通数据分析系统 智慧交通 实时监控系统 Flask框架(源码)✅
    COMSOL Multiphysics多物理场仿真应用”电化学
    Java队列相关面试题
  • 原文地址:https://www.cnblogs.com/netry/p/aspnetcore-prevent-duplicate-requests-filter-redis.html