.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,完成!

参考:https://github.com/colinin/abp-next-admin

本次演示代码 :https://github.com/applebananamilk/RedisLimitDemo

以上是关于.NetCore利用Redis实现对接口访问次数限制的主要内容,如果未能解决你的问题,请参考以下文章

分布式架构(10)---基于Redis组件的特性,实现一个分布式限流

如何用Redis实现访问次数限流?

深入Redis简单限流

Redis 实现限流策略

(十七)ATP应用测试平台——Redis实现API接口访问限流(固定窗口限流算法)

(十七)ATP应用测试平台——Redis实现API接口访问限流(固定窗口限流算法)