前言
在工作中,我们会有让客户、对接方对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的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,进行配置。
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.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,完成!
