在工作中,我们会有让客户对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的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
了!
但是连接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.Prefix}f: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,完成!
参考:https://github.com/colinin/abp-next-admin
本次演示代码 :https://github.com/applebananamilk/RedisLimitDemo