Skip to content

接口限流

限流维度

  • 分钟、小时

告警:

接口唯一标识

流程图

A[三方应用发起请求]
B[认证中间件会处理请求并验证用户身份]
C[在控制器或方法执行之前执行]
C1[记录埋点信息]
C2[本地缓存,是否已经超出限制]
C3[获取和apiRoute名称一致的限流配置]
C4[检查是否被阻止]
C5[超出80%调用预警]
C6[本地缓存,校验是否已经超出限制]
F[]

数据表

  • 接口路由 (路由名称、路由唯一标识)
  • 接口用户(用户名称、访问token、负责人)
  • 接口用户路由(用户ID、路由ID、分钟限制、小时限制)

附录

Mysql表

sql
CREATE TABLE `sys_api_route` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `route_name` varchar(50) NOT NULL DEFAULT '' COMMENT '路由名称',
  `route_key` varchar(50) NOT NULL DEFAULT '' COMMENT '路由标识',
  `remark` varchar(50) NOT NULL DEFAULT '' COMMENT '备注',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
  `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
  `create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
  `create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
  `create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
  `update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
  `update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API路由';

CREATE TABLE `sys_api_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '中文名称',
  `access_token` varchar(50) NOT NULL DEFAULT '' COMMENT '访问token',
  `owner` varchar(50) NOT NULL DEFAULT '' COMMENT '持有人',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
  `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
  `create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
  `create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
  `create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
  `update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
  `update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API用户';

CREATE TABLE `sys_api_user_route` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
  `route_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '路由ID',
  `minute_limit` int(11) NOT NULL DEFAULT '0' COMMENT '分钟限制;-1表示不限制调用量',
  `hour_limit` int(11) NOT NULL DEFAULT '0' COMMENT '小时限制;-1表示不限制调用量',
  `day_limit` int(11) NOT NULL DEFAULT '0' COMMENT '天限制;-1表示不限制调用量',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
  `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
  `create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
  `create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
  `create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
  `update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
  `update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API用户关联路由';

.Net代码实现

csharp
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
csharp
public class TokenFilterAttribute : Attribute, IActionFilter
{
    /// <summary>
    /// action执行后补充操作
    /// </summary>
    public void OnActionExecuted(ActionExecutedContext context)
    {
    }

    /// <summary>
    /// action执行前过滤操作
    /// </summary>
    public void OnActionExecuting(ActionExecutingContext context)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        if (context == null) return;

        var token = context.HttpContext.Request.Headers["token"].ToString() ?? "";
        var controllerName = context.RouteData?.Values["controller"]?.ToString()?.ToLower() ?? "";
        var actionName = context.RouteData?.Values["action"]?.ToString()?.ToLower() ?? "";
        SkynetLogger.Info($"请求的token:{token},访问地址:{controllerName}/{actionName}");

        var tokenAuthService = context.HttpContext.RequestServices.GetService<TokenAuthService>();
        if (tokenAuthService == null) return;
        var authResult = tokenAuthService.Auth(token, controllerName, actionName);
        stopwatch.Stop();
        SkynetLogger.Info($"token:{token} 验证权限耗时:{stopwatch.ElapsedMilliseconds}ms");

        if (!authResult.Result)
        {
            context.Result = new BadRequestObjectResult(new
            {
                Success = false,
                Msg = authResult.Message,
                Code = WeChatErrorType.APIException.ToInt()
            });
            return;
        }
    }
}
csharp
public class TokenAuthService
{
    private readonly IRedisClient _redisClient;
    private readonly TokenLimitBll _tokenLimitBll;
    private readonly ISysApiPermissionService _sysApiPermissionService;

    public TokenAuthService(TokenLimitBll tokenLimitBll, IRedisClientFactory redisClientFactory,
        ISysApiPermissionService sysApiPermissionService)
    {
        _tokenLimitBll = tokenLimitBll;
        _sysApiPermissionService = sysApiPermissionService;
        _redisClient = redisClientFactory.GetClient("techplatform");
    }

    public ResultModel Auth(string token, string controller, string action)
    {
        var nowTime = DateTime.Now;
        if (string.IsNullOrEmpty(token))
        {
            return ResultModel.OnFailure($"请求该接口需要在header中携带token,访问地址:{controller}/{action}");
        }

        var hourCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHH}";
        var minuteCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHHmm}";

        // 本地缓存,校验是否已经超出限制
        if (_tokenLimitBll.CheckHourKey(hourCacheKey) || _tokenLimitBll.CheckMinuteKey(minuteCacheKey))
        {
            SkynetLogger.Warn($"TOKEN:{token},请求次数超限制(缓存判断)");
            return ResultModel.OnFailure($"TOKEN:{token},请求{controller}/{action} 次数超限制");
        }

        // 获取Token用户的接口调用阈值
        var accessLimit = GetTokenLimit(token, controller, action, nowTime, 0);
        if (accessLimit.minuteLimit == 0 || accessLimit.hourLimit == 0)
        {
            SkynetLogger.Warn($"TOKEN:{token},没有接口请求次数配置");
            return ResultModel.OnFailure($"TOKEN:{token},请求{controller}/{action} 没有配置接口请求次数");
        }

        var minuteCount = _redisClient.String.Incr(minuteCacheKey);
        // 增加过期时间
        if (minuteCount == 1)
        {
            for (int i = 0; i < 10; i++)
            {
                if (_redisClient.Key.Expire(minuteCacheKey, 60 * 60)) break;
            }
        }

        if (accessLimit.minuteLimit > 0 && minuteCount > accessLimit.minuteLimit) // 每分钟限制调用次数
        {
            _tokenLimitBll.AddMinuteKey(minuteCacheKey, nowTime);
            SkynetLogger.Warn($"TOKEN:{token},请求次数超限制,限制次数{accessLimit.minuteLimit}次/min");
            return ResultModel.OnFailure($"请求次数超限制,限制次数{accessLimit.minuteLimit}次/min");
        }

        // 调用次数增加,并获取添加后的次数数据
        var hourCount = _redisClient.String.Incr(hourCacheKey);
        // 增加过期时间
        if (hourCount == 1)
        {
            for (int i = 0; i < 10; i++)
            {
                if (_redisClient.Key.Expire(hourCacheKey, 60 * 60 * 3)) break;
            }
        }

        if (accessLimit.hourLimit > 0 && hourCount > accessLimit.hourLimit) // 每小时限制调用次数
        {
            _tokenLimitBll.AddHourKey(hourCacheKey, nowTime);
            SkynetLogger.Warn($"TOKEN:{token},请求次数超限制,限制次数{accessLimit.hourLimit}次/h");
            return ResultModel.OnFailure($"请求次数超限制,限制次数{accessLimit.hourLimit}次/h");
        }

        SkynetLogger.Info(
            $"TOKEN:{token},使用次数分钟:{minuteCount}/{accessLimit.minuteLimit},小时:{hourCount}/{accessLimit.hourLimit}");

        return ResultModel.OnSuccess();
    }

    private (int minuteLimit, int hourLimit) GetTokenLimit(string token, string controller, string action,
        DateTime nowTime, int loop)
    {
        if (loop > 2)
        {
            return (-1, -1);
        }

        var userInfoCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHH}-user";
        var keyMutex = $"{userInfoCacheKey}_mutex";

        var cacheData = _redisClient.String.Get(userInfoCacheKey);
        var tempArray = cacheData?.Split('-');
        if (tempArray == null || tempArray.Length < 2)
        {
            if (_redisClient.String.SetNx(keyMutex, "1"))
            {
                (int minuteLimit, int hourLimit) result = (0, 0);
                var apiPermission = _sysApiPermissionService.GetModel(token, controller, action);
                if (apiPermission == null)
                {
                    SkynetLogger.Warn($"Token权限校验失败,token:{token},访问地址:{controller}/{action}");
                    return result;
                }

                result.minuteLimit = apiPermission.MinuteLimit;
                result.hourLimit = apiPermission.HourLimit;

                // 1小时缓存
                _redisClient.String.Set(userInfoCacheKey, $"{result.minuteLimit}-{result.hourLimit}", EX: 60 * 60);
                _redisClient.Key.Del(keyMutex);
                return result;
            }
            else
            {
                _redisClient.Key.Del(keyMutex);
            }

            Thread.Sleep(50);
            //重试
            return GetTokenLimit(token, controller, action, nowTime, loop + 1);
        }

        return (tempArray[0].ToInt(), tempArray[1].ToInt());
    }
}

上次更新时间:

最近更新