gpt4 book ai didi

ASP.NETCore使用filter和redis实现接口防重

转载 作者:我是一只小鸟 更新时间:2023-03-15 22:31:26 25 4
gpt4 key购买 nike

背景

日常开发中,经常需要对一些响应不是很快的关键业务接口增加防重功能,即短时间内收到的多个相同的请求,只处理一个,其余不处理,避免产生脏数据。这和幂等性(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 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("Please config FactorNames.");

            await next();
            return;
        }

        var idempotentKey = $"{context.HttpContext.Request.Path.Value}:{string.Join("-", factorValues)}";
        var idempotentValue = await  _cache.GetStringAsync(idempotentKey);
        if (idempotentValue != null)
        {
            _logger.LogWarning("Received duplicate request({},{}), short-circuiting...", 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 = "127.0.0.1:6379,DefaultDatabase=1";
});
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 。

最后此篇关于ASP.NETCore使用filter和redis实现接口防重的文章就讲到这里了,如果你想了解更多关于ASP.NETCore使用filter和redis实现接口防重的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com