.NetCore利用Redis实现对接口访问次数限制
Posted hiwwwk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.NetCore利用Redis实现对接口访问次数限制相关的知识,希望对你有一定的参考价值。
前言
在工作中,我们会有让客户、对接方对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的API,下载次数等这类需求。这里我们封装限制接口,使用Redis实现。
实现
首先,咱们新建一个空白解决方案RedisLimitDemo
。
新建抽象类库Limit.Abstractions
。
新建特性RequiresLimitAttribute
,来进行限制条件设置。
特性中设定了LimitName
限制名称,LimitSecond
限制时长,LimitCount
限制次数。
using System;
namespace Limit.Abstractions
/// <summary>
/// 限制特性
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class RequiresLimitAttribute : Attribute
/// <summary>
/// 限制名称
/// </summary>
public string LimitName get;
/// <summary>
/// 限制时长(秒)
/// </summary>
public int LimitSecond get;
/// <summary>
/// 限制次数
/// </summary>
public int LimitCount get;
public RequiresLimitAttribute(string limitName, int limitSecond = 1, int limitCount = 1)
if (string.IsNullOrWhiteSpace(limitName))
throw new ArgumentNullException(nameof(limitName));
LimitName = limitName;
LimitSecond = limitSecond;
LimitCount = limitCount;
新建异常类LimitValidationFailedException
对超出次数的功能,抛出统一的异常,这样利于管理及逻辑判断。
using System;
namespace Limit.Abstractions
/// <summary>
/// 限制验证失败异常
/// </summary>
public class LimitValidationFailedException : Exception
public LimitValidationFailedException(string limitName, int limitCount)
: base($"功能limitName已到最大使用上限limitCount!")
新建上下文RequiresLimitContext
类,用于各个方法之间,省的需要各种拼装参数,直接一次到位。
namespace Limit.Abstractions
/// <summary>
/// 限制验证上下文
/// </summary>
public class RequiresLimitContext
/// <summary>
/// 限制名称
/// </summary>
public string LimitName get;
/// <summary>
/// 默认限制时长(秒)
/// </summary>
public int LimitSecond get;
/// <summary>
/// 限制次数
/// </summary>
public int LimitCount get;
// 其它
public RequiresLimitContext(string limitName, int limitSecond, int limitCount)
LimitName = limitName;
LimitSecond = limitSecond;
LimitCount = limitCount;
封装验证限制次数的接口IRequiresLimitChecker
,方便进行各种实现,面向接口开发!
using System.Threading;
using System.Threading.Tasks;
namespace Limit.Abstractions
public interface IRequiresLimitChecker
/// <summary>
/// 验证
/// </summary>
/// <param name="context"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default);
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default);
现在,咱们就具备了实现限制验证的所有条件,但选择哪种方法进行验证呢?可以使用AOP动态代理,或者使用MVC的过滤器。
这里,为了方便演示,就使用IAsyncActionFilter
过滤器接口进行实现。
新建LimitValidationAsyncActionFilter
限制验证过滤器。
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Reflection;
using System.Threading.Tasks;
namespace Limit.Abstractions
/// <summary>
/// 限制验证过滤器
/// </summary>
public class LimitValidationAsyncActionFilter : IAsyncActionFilter
public IRequiresLimitChecker RequiresLimitChecker get;
public LimitValidationAsyncActionFilter(IRequiresLimitChecker requiresLimitChecker)
RequiresLimitChecker = requiresLimitChecker;
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
// 获取特性
var limitAttribute = GetRequiresLimitAttribute(GetMethodInfo(context));
if (limitAttribute == null)
await next();
return;
// 组装上下文
var requiresLimitContext = new RequiresLimitContext(limitAttribute.LimitName, limitAttribute.LimitSecond, limitAttribute.LimitCount);
// 检查
await PreCheckAsync(requiresLimitContext);
// 执行方法
await next();
// 次数自增
await PostCheckAsync(requiresLimitContext);
protected virtual MethodInfo GetMethodInfo(ActionExecutingContext context)
return (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo;
/// <summary>
/// 获取限制特性
/// </summary>
/// <returns></returns>
protected virtual RequiresLimitAttribute GetRequiresLimitAttribute(MethodInfo methodInfo)
return methodInfo.GetCustomAttribute<RequiresLimitAttribute>();
/// <summary>
/// 验证之前
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected virtual async Task PreCheckAsync(RequiresLimitContext context)
bool isAllowed = await RequiresLimitChecker.CheckAsync(context);
if (!isAllowed)
throw new LimitValidationFailedException(context.LimitName, context.LimitCount);
/// <summary>
/// 验证之后
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected virtual async Task PostCheckAsync(RequiresLimitContext context)
await RequiresLimitChecker.ProcessAsync(context);
逻辑看起来非常简单。
首先,需要判断执行的方法是否进行了限制,就是有没有标注RequiresLimitAttribute
这个特性,如果没有就直接执行。否则的话,需要在执行方法之前判断是否能执行方法,执行之后需要让使用次数进行+1操作。
上面就是基础接口的定义,接下来咱们需要接入Redis
,实现具体的判断和使用次数自增。
新建类库Limit.Redis
新建选项类RedisRequiresLimitOptions
,因为咱们也不知道Redis连接方式是什么,这样就需要在使用的时候进行配置。
using Microsoft.Extensions.Options;
namespace Limit.Redis
public class RedisRequiresLimitOptions : IOptions<RedisRequiresLimitOptions>
/// <summary>
/// Redis连接字符串
/// </summary>
public string Configuration get; set;
/// <summary>
/// Key前缀
/// </summary>
public string Prefix get; set;
public RedisRequiresLimitOptions Value => this;
这里,使用了Configuration
来进行配置连接字符串,有时候咱们会需要对Key加上前缀,方便查找或者进行模块划分,所以加上Prefix
前缀。
有了配置,就可以连接Redis
了!
这里使用开源类库StackExchange.Redis
来进行操作。
新建实现类RedisRequiresLimitChecker
using Limit.Abstractions;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Limit.Redis
public class RedisRequiresLimitChecker : IRequiresLimitChecker
protected RedisRequiresLimitOptions Options get;
private IDatabaseAsync _database;
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
public RedisRequiresLimitChecker(IOptions<RedisRequiresLimitOptions> options)
if (options == null)
throw new ArgumentNullException(nameof(options));
Options = options.Value;
public async Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default)
await ConnectAsync();
if (await _database.KeyExistsAsync(CalculateCacheKey(context)))
var result = await _database.StringGetAsync(CalculateCacheKey(context));
return (int)result + 1 <= context.LimitCount;
else
return true;
public async Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default)
await ConnectAsync();
string cacheKey = CalculateCacheKey(context);
if (await _database.KeyExistsAsync(cacheKey))
await _database.StringIncrementAsync(cacheKey);
else
await _database.StringSetAsync(cacheKey, "1", new TimeSpan(0, 0, context.LimitSecond), When.Always);
protected virtual string CalculateCacheKey(RequiresLimitContext context)
return $"Options.Prefixf:RedisRequiresLimitChecker,ln:context.LimitName";
protected virtual async Task ConnectAsync(CancellationToken cancellation = default)
cancellation.ThrowIfCancellationRequested();
if (_database != null)
return;
// 控制并发
await _connectionLock.WaitAsync(cancellation);
try
if (_database == null)
var connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration);
_database = connection.GetDatabase();
finally
_connectionLock.Release();
逻辑也是简单的逻辑,就不多解释了。不过这里的命令在高并发的情况下执行起来可能会有间隙,还可以进行优化一下。
实现咱们有了,接下来就要写扩展方法方便调用。
新建扩展方法类ServiceCollectionExtensions
,记得命名空间要在Microsoft.Extensions.DependencyInjection
下面,不然使用的时候找这个方法也是一个问题。
using Limit.Abstractions;
using Limit.Redis;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
namespace Microsoft.Extensions.DependencyInjection
public static class ServiceCollectionExtensions
/// <summary>
/// 添加Redis功能限制验证
/// </summary>
/// <param name="services"></param>
/// <param name="options"></param>
public static void AddRedisLimitValidation(this IServiceCollection services, Action<RedisRequiresLimitOptions> options)
services.Replace(ServiceDescriptor.Singleton<IRequiresLimitChecker, RedisRequiresLimitChecker>());
services.Configure(options);
services.Configure<MvcOptions>(mvcOptions =>
mvcOptions.Filters.Add<LimitValidationAsyncActionFilter>();
);
至此,全部结束,我们开始去进行测试验证。
新建.Net Core Web API
项目LimitTestWebApi
引入咱们写好的类库Limit.Redis
然后在Program
类中,注入写好的服务。
直接就用模板自带的Controller
进行测试吧
咱们让他60秒内只能访问5次!
启动项目开始测试。
首先执行一次。
查看Redis中的数据。
再快速执行5次。
Redis中数据。
缓存剩余时间。
咱们等到缓存时间结束再次执行。
ok,完成!
以上是关于.NetCore利用Redis实现对接口访问次数限制的主要内容,如果未能解决你的问题,请参考以下文章
分布式架构(10)---基于Redis组件的特性,实现一个分布式限流