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 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为防重因子。
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实现接口防重的更多相关文章
- Asp.net Core 缓存 MemoryCache 和 Redis
Asp.net Core 缓存 MemoryCache 和 Redis 目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 经过 N 久反复的尝试,翻阅了网上无数的资料,GitH ...
- ASP.Net Core使用分布式缓存Redis从入门到实战演练
一.课程介绍 人生苦短,我用.NET Core!缓存在很多情况下需要用到,合理利用缓存可以一方面可以提高程序的响应速度,同时可以减少对特定资源访问的压力. 所以经常要用到且不会频繁改变且被用户共享的 ...
- asp.net core使用Swashbuckle.AspNetCore(swagger)生成接口文档
asp.net core中使用Swashbuckle.AspNetCore(swagger)生成接口文档 Swashbuckle.AspNetCore:swagger的asp.net core实现 项 ...
- Asp.Net Core基于JWT认证的数据接口网关Demo
近日,应一位朋友的邀请写了个Asp.Net Core基于JWT认证的数据接口网关Demo.朋友自己开了个公司,接到的一个升级项目,客户要求用Aps.Net Core做数据网关服务且基于JWT认证实现对 ...
- 给 asp.net core 写个中间件来记录接口耗时
给 asp.net core 写个中间件来记录接口耗时 Intro 写接口的难免会遇到别人说接口比较慢,到底慢多少,一个接口服务器处理究竟花了多长时间,如果能有具体的数字来记录每个接口耗时多少,别人再 ...
- 【无私分享:ASP.NET CORE 项目实战(第十一章)】Asp.net Core 缓存 MemoryCache 和 Redis
目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 经过 N 久反复的尝试,翻阅了网上无数的资料,GitHub上下载了十几个源码参考, Memory 和 Redis 终于写出一个 ...
- 【ASP.NET Core】选项模式的相关接口
在 .NET 中,配置与选项模式其实有联系的(这些功能现在不仅限于 ASP.NET Core,而是作为平台扩展来提供,在其他.NET 项目中都能用).配置一般从多个来源(上一篇水文中的例子,记得否?) ...
- 记Asp.Net Core Swagger 使用 并带域接口处理
引用作者原话:Asp.Net的WebApi中使用Swagger作为说明和测试的页面是非常不错的,比起WebApiTestClient来至少在界面上的很大的提升.但是使用Swagger时如果只是一般的控 ...
- Asp.Net Core 使用Quartz基于界面画接口管理做定时任务
今天抽出一点点时间来造一个小轮子,是关于定时任务这块的. 这篇文章主要从一下几点介绍: 创建数据库管理表 创建web项目 引入quarzt nuget 包 写具体配置操作,实现定时任务处理 第一步:创 ...
- ASP.NET Core 3.1使用Swagger API接口文档
Swagger是最流行的API开发工具,它遵循了OpenAPI规范,可以根据API接口自动生成在线文档,这样就可以解决文档更新不及时的问题.它可以贯穿于整个API生态,比如API的设计.编写API文档 ...
随机推荐
- SSIS Package Version
当 SSIS 首次出现时,有大量关于所有问题的笑话和帖子,以及每个人如何认为 DTS 更好,他们真的必须转换吗?多年来,我开始欣赏 SSIS.它是一个非常强大和有用的工具,可以做一些了不起的事情.当然 ...
- [2002年NOIP提高组] 均分纸牌
有 N 堆纸牌,编号分别为 1,2,-, N.每堆上有若干张,但纸牌总数必为 N 的倍数.可以在任一堆上取若于张纸牌,然后移动.移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上:在编 ...
- 解决java.sql.SQLException: null, message from server: "Host 'XXX' is not allowed to connect异常
Sqoop连接MySQL报异常.这个异常是数据库只允许localhost或127.0.0.1访问,不允许远程访问.我用的本机IP都不行. 解决办法:修改访问权限即可. 打开cmd,进入mysql fl ...
- 关于js闭包的基础理解
闭包 拿一个可以记录函数调用次数的来进行理解,如下方 let n = 0 function numUp(){ n++ console.log(n) } const fn = numUp() fn() ...
- 关于集合set、数据类型转换等
- 流量运营(pvuv跳出率漏斗abtest)
AARRR模型 1. 激活: 拉新,对用户来源渠道进行分析,哪些合作.投放渠道对我们产品更合适 2. 注册: 流量激活之后,如果用户只是点进来就走了,也没什么用,合适的产品功能切合用户需求,才会有转化 ...
- 如何为linux kernel贡献代码
参考: Linux内核文档:<如何让你的改动进入内核> - 广漠飘羽 - 博客园 提交内核补丁到Linux社区的步骤 - 广漠飘羽 - 博客园 建议: 内容具有时效性,需要阅读最新版本的同 ...
- 【已解决】robotframework 连接oracle数据库返回结果中文显示乱码
问题描述:查询数据库返回信息有中文的时候会显示unicode的样式,如图: 环境:robotframework 3.0.x 解决方法: 找到Python安装目录下的\Lib\site-packages ...
- 机器学习实战—搭建BP神经网络实现手写数字识别
看了几天的BP神经网络,总算是对它有一点点的理解了.今天就用python搭建了一个模型来实现手写数字的识别. 一.BP神经网络简介 BP(back propagation)神经网络是一种按照误差逆向传 ...
- MLP-Mixer: An all-MLP Architecture for Vision 纯MLP架构
典型的MLP结构包括三层:input.hidden.output.不同层之间都是全联接的. MLP-Mixer完全利用基础的矩阵乘法运算和数据变换以及非线性层来完成复杂数据集的分类任务. Step 1 ...