背景

日常开发中,经常需要对一些响应不是很快的关键业务接口增加防重功能,即短时间内收到的多个相同的请求,只处理一个,其余不处理,避免产生脏数据。这和幂等性(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 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中已经实现,由于它需要注入 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<PreventDuplicateRequestsActionFilter>();
filter.FactorNames = _factorNames;
filter.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expiredMinutes);
return filter;
}
public bool IsReusable => false;
}

注册

为了简单,操作redis,直接使用微软Microsoft.Extensions.Caching.StackExchangeRedis包;注册PreventDuplicateRequestsActionFilterPreventDuplicateRequestsAttribute无需注册。

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "127.0.0.1:6379,DefaultDatabase=1";
});
builder.Services.AddScoped<PreventDuplicateRequestsActionFilter>();

使用

假设我们有一个接口CancelOrder,我们指定入参中的OrderId为防重因子。

namespace PreventDuplicateRequestDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
[HttpPost(nameof(CancelOrder))]
[PreventDuplicateRequests(5, "OrderId")]
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,除第一次调用成功,其余请求皆被短路

参考链接

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.NET Core使用filter和redis实现接口防重的更多相关文章

  1. Asp.net Core 缓存 MemoryCache 和 Redis

    Asp.net Core 缓存 MemoryCache 和 Redis 目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 经过 N 久反复的尝试,翻阅了网上无数的资料,GitH ...

  2. ASP.Net Core使用分布式缓存Redis从入门到实战演练

    一.课程介绍 人生苦短,我用.NET Core!缓存在很多情况下需要用到,合理利用缓存可以一方面可以提高程序的响应速度,同时可以减少对特定资源访问的压力.  所以经常要用到且不会频繁改变且被用户共享的 ...

  3. asp.net core使用Swashbuckle.AspNetCore(swagger)生成接口文档

    asp.net core中使用Swashbuckle.AspNetCore(swagger)生成接口文档 Swashbuckle.AspNetCore:swagger的asp.net core实现 项 ...

  4. Asp.Net Core基于JWT认证的数据接口网关Demo

    近日,应一位朋友的邀请写了个Asp.Net Core基于JWT认证的数据接口网关Demo.朋友自己开了个公司,接到的一个升级项目,客户要求用Aps.Net Core做数据网关服务且基于JWT认证实现对 ...

  5. 给 asp.net core 写个中间件来记录接口耗时

    给 asp.net core 写个中间件来记录接口耗时 Intro 写接口的难免会遇到别人说接口比较慢,到底慢多少,一个接口服务器处理究竟花了多长时间,如果能有具体的数字来记录每个接口耗时多少,别人再 ...

  6. 【无私分享:ASP.NET CORE 项目实战(第十一章)】Asp.net Core 缓存 MemoryCache 和 Redis

    目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 经过 N 久反复的尝试,翻阅了网上无数的资料,GitHub上下载了十几个源码参考, Memory 和 Redis 终于写出一个 ...

  7. 【ASP.NET Core】选项模式的相关接口

    在 .NET 中,配置与选项模式其实有联系的(这些功能现在不仅限于 ASP.NET Core,而是作为平台扩展来提供,在其他.NET 项目中都能用).配置一般从多个来源(上一篇水文中的例子,记得否?) ...

  8. 记Asp.Net Core Swagger 使用 并带域接口处理

    引用作者原话:Asp.Net的WebApi中使用Swagger作为说明和测试的页面是非常不错的,比起WebApiTestClient来至少在界面上的很大的提升.但是使用Swagger时如果只是一般的控 ...

  9. Asp.Net Core 使用Quartz基于界面画接口管理做定时任务

    今天抽出一点点时间来造一个小轮子,是关于定时任务这块的. 这篇文章主要从一下几点介绍: 创建数据库管理表 创建web项目 引入quarzt nuget 包 写具体配置操作,实现定时任务处理 第一步:创 ...

  10. ASP.NET Core 3.1使用Swagger API接口文档

    Swagger是最流行的API开发工具,它遵循了OpenAPI规范,可以根据API接口自动生成在线文档,这样就可以解决文档更新不及时的问题.它可以贯穿于整个API生态,比如API的设计.编写API文档 ...

随机推荐

  1. 配置RMAN(缩减版)

    配置备份的默认类型:备份集或副本 要配置默认备份类型: 启动 RMAN 并连接到目标数据库和恢复目录(如果使用). 将备份集或映像副本配置为默认备份类型. 以下示例配置磁盘备份到副本和备份集的备份类型 ...

  2. *args、**kwargs参数组

    '''def test(*args): # *agrs接收的是N个位置参数,不能接受关键字参数,转化成元祖 print(args)test(1,2,3,4,5,6)test(*[1,2,4,5,5]) ...

  3. A Super Hero

    DP? QwQ这题似乎不能直接贪心2333-- 阶段 很明显的阶段性,\(n\)关便为\(n\)个阶段, 状态 分好阶段后,容易构造出状态的表达: \(f[i,j]\)表示Ma5termind在最开始 ...

  4. CSR生成证书与签发证书

    openSSL生成证书 生成根证书 // Generate CA private key openssl genrsa -out ca.key 2048 // Generate CSR openssl ...

  5. 20200925--矩阵加法(奥赛一本通P93 6 多维数组)

    输入两个n行m列的矩阵A和B,输出它们的和A+B 输入: 第1行包含两个整数n和m(1<=n<=100,1<=m<=100),表示矩阵的行数和列数. 接下来n行,每行m个整数, ...

  6. uniapp相关

    1.uniapp官网 网址:https://uniapp.dcloud.net.cn/ 2.引入组件库 网址:https://www.uviewui.com/ 3.问题如下 (1)使用SwipeAct ...

  7. 【项目记录】1:一些没有一次安装成功地Python模块

    记录一下,下次查找方便. 1.PIL包 PIL名称已经换成了Pillow 所以使用: pip3 install pil 会报错. 正确方法是: pip3 install pillow 2.win32c ...

  8. StarUML画用例图

    好久没画图在StarUML里都找不到useCase了,记下来不然又忘了 右击Untitled->Add Diagram->UseCase Diagram,然后左边就有UseCase了就可以 ...

  9. Java中String相关知识

    String 1.String概述 String代表字符串,Java程序中所有的字符串文字(例如'abc")都被实现为此类的实例,也就是说,Java中所有的双引号字符串都是String类的对 ...

  10. GDB 调试 - 正确地加载调试符号文件

    一.开发流程 1. 编译可执行文件 1 #include <stdio.h> 2 #include <unistd.h> 3 4 void test() 5 { 6 char ...