.NetCore利用Redis實現對接口訪問次數限制


前言

在工作中,我們會有讓客戶、對接方對某一接口或某一項功能,需要限制使用的次數,比如獲取某個數據的API,下載次數等這類需求。這里我們封裝限制接口,使用Redis實現。


實現

首先,新建一個空白解決方案RedisLimitDemo
image.png
新建抽象類庫Limit.Abstractions
image.png
image.png

新建特性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 allowed = 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
image.png
image.png
新建選項類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.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();
            }
        }
    }
}

邏輯也是簡單的邏輯,就不多解釋了。不過這里的命令在高並發的情況下執行起來可能會有間隙,可以使用Lua腳本,保證是原子操作。

實現有了,接下來就要寫擴展方法方便調用。
新建擴展方法類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
image.png
image.png
引入咱們寫好的類庫Limit.Redis

然后在Program類中,注入寫好的服務。
image.png
直接就用模板自帶的Controller進行測試吧
image.png
image.png
此處限制60秒內只能訪問5次。

啟動項目開始測試。
image.png
首先執行一次。
image.png
查看Redis中的數據。
image.png
再快速執行5次。
image.png
Redis中數據。
image.png
緩存剩余時間。
image.png
咱們等到緩存時間結束再次執行。
image.png
ok,完成!

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM