前言

在工作中,我们会有让客户、对接方对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的API,下载次数等这类需求。这里我们封装限制接口,使用Redis实现。

实现

首先,新建一个空白解决方案RedisLimitDemo
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图1
新建抽象类库Limit.Abstractions
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图2
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图3

新建特性RequiresLimitAttribute,来进行限制条件设置。
特性中设定了LimitName限制名称,LimitSecond限制时长,LimitCount限制次数。

  1. using System;
  2. namespace Limit.Abstractions
  3. {
  4. /// <summary>
  5. /// 限制特性
  6. /// </summary>
  7. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
  8. public class RequiresLimitAttribute : Attribute
  9. {
  10. /// <summary>
  11. /// 限制名称
  12. /// </summary>
  13. public string LimitName { get; }
  14. /// <summary>
  15. /// 限制时长(秒)
  16. /// </summary>
  17. public int LimitSecond { get; }
  18. /// <summary>
  19. /// 限制次数
  20. /// </summary>
  21. public int LimitCount { get; }
  22. public RequiresLimitAttribute(string limitName, int limitSecond = 1, int limitCount = 1)
  23. {
  24. if (string.IsNullOrWhiteSpace(limitName))
  25. {
  26. throw new ArgumentNullException(nameof(limitName));
  27. }
  28. LimitName = limitName;
  29. LimitSecond = limitSecond;
  30. LimitCount = limitCount;
  31. }
  32. }
  33. }

新建异常类LimitValidationFailedException对超出次数的功能,抛出统一的异常,这样利于管理及逻辑判断。

  1. using System;
  2. namespace Limit.Abstractions
  3. {
  4. /// <summary>
  5. /// 限制验证失败异常
  6. /// </summary>
  7. public class LimitValidationFailedException : Exception
  8. {
  9. public LimitValidationFailedException(string limitName, int limitCount)
  10. : base($"功能{limitName}已到最大使用上限{limitCount}!")
  11. {
  12. }
  13. }
  14. }

新建上下文RequiresLimitContext类,用于各个方法之间,省的需要各种拼装参数,直接一次到位。

  1. namespace Limit.Abstractions
  2. {
  3. /// <summary>
  4. /// 限制验证上下文
  5. /// </summary>
  6. public class RequiresLimitContext
  7. {
  8. /// <summary>
  9. /// 限制名称
  10. /// </summary>
  11. public string LimitName { get; }
  12. /// <summary>
  13. /// 默认限制时长(秒)
  14. /// </summary>
  15. public int LimitSecond { get; }
  16. /// <summary>
  17. /// 限制次数
  18. /// </summary>
  19. public int LimitCount { get; }
  20. // 其它
  21. public RequiresLimitContext(string limitName, int limitSecond, int limitCount)
  22. {
  23. LimitName = limitName;
  24. LimitSecond = limitSecond;
  25. LimitCount = limitCount;
  26. }
  27. }
  28. }

封装验证限制次数的接口IRequiresLimitChecker,方便进行各种实现,面向接口开发!

  1. using System.Threading;
  2. using System.Threading.Tasks;
  3. namespace Limit.Abstractions
  4. {
  5. public interface IRequiresLimitChecker
  6. {
  7. /// <summary>
  8. /// 验证
  9. /// </summary>
  10. /// <param name="context"></param>
  11. /// <param name="cancellation"></param>
  12. /// <returns></returns>
  13. Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default);
  14. /// <summary>
  15. ///
  16. /// </summary>
  17. /// <param name="context"></param>
  18. /// <param name="cancellation"></param>
  19. /// <returns></returns>
  20. Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default);
  21. }
  22. }

现在,具备了实现限制验证的所有条件,但选择哪种方法进行验证呢?可以使用AOP动态代理,或者使用MVC的过滤器
这里,为了方便演示,就使用IAsyncActionFilter过滤器接口进行实现。

新建LimitValidationAsyncActionFilter限制验证过滤器。

  1. using Microsoft.AspNetCore.Mvc.Controllers;
  2. using Microsoft.AspNetCore.Mvc.Filters;
  3. using System.Reflection;
  4. using System.Threading.Tasks;
  5. namespace Limit.Abstractions
  6. {
  7. /// <summary>
  8. /// 限制验证过滤器
  9. /// </summary>
  10. public class LimitValidationAsyncActionFilter : IAsyncActionFilter
  11. {
  12. public IRequiresLimitChecker RequiresLimitChecker { get; }
  13. public LimitValidationAsyncActionFilter(IRequiresLimitChecker requiresLimitChecker)
  14. {
  15. RequiresLimitChecker = requiresLimitChecker;
  16. }
  17. public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
  18. {
  19. // 获取特性
  20. var limitAttribute = GetRequiresLimitAttribute(GetMethodInfo(context));
  21. if (limitAttribute == null)
  22. {
  23. await next();
  24. return;
  25. }
  26. // 组装上下文
  27. var requiresLimitContext = new RequiresLimitContext(limitAttribute.LimitName, limitAttribute.LimitSecond, limitAttribute.LimitCount);
  28. // 检查
  29. await PreCheckAsync(requiresLimitContext);
  30. // 执行方法
  31. await next();
  32. // 次数自增
  33. await PostCheckAsync(requiresLimitContext);
  34. }
  35. protected virtual MethodInfo GetMethodInfo(ActionExecutingContext context)
  36. {
  37. return (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo;
  38. }
  39. /// <summary>
  40. /// 获取限制特性
  41. /// </summary>
  42. /// <returns></returns>
  43. protected virtual RequiresLimitAttribute GetRequiresLimitAttribute(MethodInfo methodInfo)
  44. {
  45. return methodInfo.GetCustomAttribute<RequiresLimitAttribute>();
  46. }
  47. /// <summary>
  48. /// 验证之前
  49. /// </summary>
  50. /// <param name="context"></param>
  51. /// <returns></returns>
  52. protected virtual async Task PreCheckAsync(RequiresLimitContext context)
  53. {
  54. bool isAllowed = await RequiresLimitChecker.CheckAsync(context);
  55. if (!isAllowed)
  56. {
  57. throw new LimitValidationFailedException(context.LimitName, context.LimitCount);
  58. }
  59. }
  60. /// <summary>
  61. /// 验证之后
  62. /// </summary>
  63. /// <param name="context"></param>
  64. /// <returns></returns>
  65. protected virtual async Task PostCheckAsync(RequiresLimitContext context)
  66. {
  67. await RequiresLimitChecker.ProcessAsync(context);
  68. }
  69. }
  70. }

逻辑看起来非常简单。
首先,需要判断执行的方法是否进行了限制,就是有没有标注RequiresLimitAttribute这个特性,如果没有就直接执行。否则的话,需要在执行方法之前判断是否能执行方法,执行之后需要让使用次数进行**+1**操作。

上面就是基础接口的定义,接下来需要接入Redis,实现具体的判断和使用次数自增。

新建类库Limit.Redis
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图4
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图5
新建选项类RedisRequiresLimitOptions,进行配置。

  1. using Microsoft.Extensions.Options;
  2. namespace Limit.Redis
  3. {
  4. public class RedisRequiresLimitOptions : IOptions<RedisRequiresLimitOptions>
  5. {
  6. /// <summary>
  7. /// Redis连接字符串
  8. /// </summary>
  9. public string Configuration { get; set; }
  10. /// <summary>
  11. /// Key前缀
  12. /// </summary>
  13. public string Prefix { get; set; }
  14. public RedisRequiresLimitOptions Value => this;
  15. }
  16. }

这里,使用了Configuration来进行配置连接字符串,有时候会需要对Key加上前缀,方便查找或者进行模块划分,所以加上Prefix前缀。

有了配置,就可以连接Redis了!
这里使用开源类库StackExchange.Redis来进行操作。

新建实现类RedisRequiresLimitChecker

  1. using Limit.Abstractions;
  2. using Microsoft.Extensions.Options;
  3. using StackExchange.Redis;
  4. using System;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. namespace Limit.Redis
  8. {
  9. public class RedisRequiresLimitChecker : IRequiresLimitChecker
  10. {
  11. protected RedisRequiresLimitOptions Options { get; }
  12. private IDatabaseAsync _database;
  13. private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
  14. public RedisRequiresLimitChecker(IOptions<RedisRequiresLimitOptions> options)
  15. {
  16. if (options == null)
  17. {
  18. throw new ArgumentNullException(nameof(options));
  19. }
  20. Options = options.Value;
  21. }
  22. public async Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default)
  23. {
  24. await ConnectAsync();
  25. if (await _database.KeyExistsAsync(CalculateCacheKey(context)))
  26. {
  27. var result = await _database.StringGetAsync(CalculateCacheKey(context));
  28. return (int)result + 1 <= context.LimitCount;
  29. }
  30. else
  31. {
  32. return true;
  33. }
  34. }
  35. public async Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default)
  36. {
  37. await ConnectAsync();
  38. string cacheKey = CalculateCacheKey(context);
  39. if (await _database.KeyExistsAsync(cacheKey))
  40. {
  41. await _database.StringIncrementAsync(cacheKey);
  42. }
  43. else
  44. {
  45. await _database.StringSetAsync(cacheKey, "1", new TimeSpan(0, 0, context.LimitSecond), When.Always);
  46. }
  47. }
  48. protected virtual string CalculateCacheKey(RequiresLimitContext context)
  49. {
  50. return $"{Options.Prefix}f:RedisRequiresLimitChecker,ln:{context.LimitName}";
  51. }
  52. protected virtual async Task ConnectAsync(CancellationToken cancellation = default)
  53. {
  54. cancellation.ThrowIfCancellationRequested();
  55. if (_database != null)
  56. {
  57. return;
  58. }
  59. // 控制并发
  60. await _connectionLock.WaitAsync(cancellation);
  61. try
  62. {
  63. if (_database == null)
  64. {
  65. var connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration);
  66. _database = connection.GetDatabase();
  67. }
  68. }
  69. finally
  70. {
  71. _connectionLock.Release();
  72. }
  73. }
  74. }
  75. }

逻辑也是简单的逻辑,就不多解释了。不过这里的命令在高并发的情况下执行起来可能会有间隙,还可以进行优化一下。

实现有了,接下来就要写扩展方法方便调用。
新建扩展方法类ServiceCollectionExtensions,记得命名空间要在Microsoft.Extensions.DependencyInjection下面,不然使用的时候找这个方法也是一个问题。

  1. using Limit.Abstractions;
  2. using Limit.Redis;
  3. using Microsoft.AspNetCore.Mvc;
  4. using Microsoft.Extensions.DependencyInjection.Extensions;
  5. using System;
  6. namespace Microsoft.Extensions.DependencyInjection
  7. {
  8. public static class ServiceCollectionExtensions
  9. {
  10. /// <summary>
  11. /// 添加Redis功能限制验证
  12. /// </summary>
  13. /// <param name="services"></param>
  14. /// <param name="options"></param>
  15. public static void AddRedisLimitValidation(this IServiceCollection services, Action<RedisRequiresLimitOptions> options)
  16. {
  17. services.Replace(ServiceDescriptor.Singleton<IRequiresLimitChecker, RedisRequiresLimitChecker>());
  18. services.Configure(options);
  19. services.Configure<MvcOptions>(mvcOptions =>
  20. {
  21. mvcOptions.Filters.Add<LimitValidationAsyncActionFilter>();
  22. });
  23. }
  24. }
  25. }

至此,全部结束,开始去进行测试验证。

新建.Net Core Web API项目LimitTestWebApi
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图6
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图7
引入写好的类库Limit.Redis

然后在Program类中,注入写好的服务。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图8
直接就用模板自带的Controller进行测试吧
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图9
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图10
让它60秒内只能访问5次。

启动项目开始测试。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图11
首先执行一次。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图12
查看Redis中的数据。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图13
再快速执行5次。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图14
Redis中数据。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图15
缓存剩余时间。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图16
咱们等到缓存时间结束再次执行。
08.02-.NetCore利用Redis实现对接口访问次数限制 - 图17
ok,完成!

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

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