前言
在工作中,我們會有讓客戶、對接方對某一接口或某一項功能,需要限制使用的次數,比如獲取某個數據的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 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
新建選項類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
引入咱們寫好的類庫Limit.Redis
然后在Program
類中,注入寫好的服務。
直接就用模板自帶的Controller
進行測試吧
此處限制60秒內只能訪問5次。
啟動項目開始測試。
首先執行一次。
查看Redis中的數據。
再快速執行5次。
Redis中數據。
緩存剩余時間。
咱們等到緩存時間結束再次執行。
ok,完成!