搜索

Redis 实现多规则限流的思考与实践

发表于 2025-11-05 08:14:45 来源:益强智未来

简介

市面上很多介绍redis如何实现限流的现多限流,但是规则大部分都有一个缺点,就是考实只能实现单一的限流,比如1分钟访问1次或者60分钟访问10次这种,现多限流但是规则如果想一个接口两种规则都需要满足呢,我们的考实项目又是分布式项目,应该如何解决,现多限流下面就介绍一下redis实现分布式多规则限流的规则方式。

思考

如何一分钟只能发送一次验证码,考实一小时只能发送10次验证码等等多种规则的现多限流限流如何防止接口被恶意打击(短时间内大量请求)如何限制接口规定时间内访问次数

解决方法

记录某IP访问次数

使用 String结构 记录固定时间段内某用户IP访问某接口的次数

RedisKey = prefix : className : methodNameRedisVlue = 访问次数

拦截请求:

初次访问时设置 「[RedisKey] [RedisValue=1] [规定的过期时间]」获取 RedisValue 是否超过规定次数,超过则拦截,规则未超过则对 RedisKey 进行加1

分析: 规则是考实每分钟访问 1000 次

考虑并发问题

假设目前 RedisKey => RedisValue 为 999

目前大量请求进行到第一步( 获取Redis请求次数 ),那么所有线程都获取到了值为999,现多限流进行判断都未超过限定次数则不拦截,规则导致实际次数超过 1000 次

「解决办法:」 保证方法执行原子性(加锁、免费信息发布网考实lua)

考虑在临界值进行访问

思考下图

图片

代码实现: 比较简单

参考:https://gitee.com/y_project/RuoYi-Vue/blob/master/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java。

Zset解决临界值问题

使用 Zset 进行存储,解决临界值访问问题

图片

网上几乎都有实现,这里就不过多介绍

实现多规则限流

先确定最终需要的效果

能实现多种限流规则能实现防重复提交

通过以上要求设计注解(先想象出最终实现效果)

复制@RateLimiter( rules = { // 60秒内只能访问10次 @RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS), // 120秒内只能访问20次 @RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS) }, // 防重复提交 (5秒钟只能访问1次) preventDuplicate = true )1.2.3.4.5.6.7.8.9.10.11.

编写注解(RateLimiter,RateRule)

编写 RateLimiter 注解。

复制/** * @Description: 请求接口限制 * @Author: yiFei */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RateLimiter { /** * 限流key */ String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX; /** * 限流类型 ( 默认 Ip 模式 ) */ LimitTypeEnum limitType() default LimitTypeEnum.IP; /** * 错误提示 */ ResultCode message() default ResultCode.REQUEST_MORE_ERROR; /** * 限流规则 (规则不可变,可多规则) */ RateRule[] rules() default {}; /** * 防重复提交值 */ boolean preventDuplicate() default false; /** * 防重复提交默认值 */ RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.

编写RateRule注解

复制@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RateRule { /** * 限流次数 */ long count() default 10; /** * 限流时间 */ long time() default 60; /** * 限流时间单位 */ TimeUnit timeUnit() default TimeUnit.SECONDS; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

拦截注解 RateLimiter

确定redis存储方式

RedisKey = prefix : className : methodName

RedisScore = 时间戳

RedisValue = 任意分布式不重复的值即可

编写生成 RedisKey 的方法 复制/** * 通过 rateLimiter 和 joinPoint 拼接 prefix : ip / userId : classSimpleName - methodName * * @param rateLimiter 提供 prefix * @param joinPoint 提供 classSimpleName : methodName * @return */ public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) { StringBuffer key = new StringBuffer(rateLimiter.key()); // 不同限流类型使用不同的前缀 switch (rateLimiter.limitType()) { // XXX 可以新增通过参数指定参数进行限流 case IP: key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":"); break; case USER_ID: SysUserDetails user = SecurityUtil.getUser(); if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":"); break; case GLOBAL: break; } MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); key.append(targetClass.getSimpleName()).append("-").append(method.getName()); return key.toString(); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

编写lua脚本

编写lua脚本 (两种将时间添加到Redis的方法)。

Zset的UUID value值

UUID(可用其他有相同的特性的值)为Zset中的value值

参数介绍

KEYS[1] = prefix : ? : className : methodName

KEYS[2] = 唯一ID

KEYS[3] = 当前时间

ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 ...]

由java传入分布式不重复的 value 值 复制-- 1. 获取参数 local key = KEYS[1] local uuid = KEYS[2] local currentTime = tonumber(KEYS[3]) -- 2. 以数组最大值为 ttl 最大值 local expireTime = -1; -- 3. 遍历数组查看是否超过限流规则 for i = 1, #ARGV, 2 do local rateRuleCount = tonumber(ARGV[i]) local rateRuleTime = tonumber(ARGV[i + 1]) -- 3.1 判断在单位时间内访问次数 local count = redis.call(ZCOUNT, key, currentTime - rateRuleTime, currentTime) -- 3.2 判断是否超过规定次数 if tonumber(count) >= rateRuleCount then return true end -- 3.3 判断元素最大值,设置为最终过期时间 if rateRuleTime > expireTime then expireTime = rateRuleTime end end -- 4. redis 中添加当前时间 redis.call(ZADD, key, currentTime, uuid) -- 5. 更新缓存过期时间 redis.call(PEXPIRE, key, expireTime) -- 6. 删除最大时间限度之前的数据,防止数据过多 redis.call(ZREMRANGEBYSCORE, key, 0, currentTime - expireTime) return false1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

根据时间戳作为Zset中的value值

参数介绍

KEYS[1] = prefix : ? : className : methodName

KEYS[2] = 当前时间

ARGV = [次数,IT技术网单位时间,次数,单位时间, 次数, 单位时间 ...]

根据时间进行生成value值,考虑同一毫秒添加相同时间值问题

以下为第二种实现方式,在并发高的情况下效率低,value是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call(ZADD, key, currentTime, currentTime),但是在不冲突value的情况下,会比生成 UUID 好

复制-- 1. 获取参数 local key = KEYS[1] local currentTime = KEYS[2] -- 2. 以数组最大值为 ttl 最大值 local expireTime = -1; -- 3. 遍历数组查看是否越界 for i = 1, #ARGV, 2 do local rateRuleCount = tonumber(ARGV[i]) local rateRuleTime = tonumber(ARGV[i + 1]) -- 3.1 判断在单位时间内访问次数 local count = redis.call(ZCOUNT, key, currentTime - rateRuleTime, currentTime) -- 3.2 判断是否超过规定次数 if tonumber(count) >= rateRuleCount then return true end -- 3.3 判断元素最大值,设置为最终过期时间 if rateRuleTime > expireTime then expireTime = rateRuleTime end end -- 4. 更新缓存过期时间 redis.call(PEXPIRE, key, expireTime) -- 5. 删除最大时间限度之前的数据,防止数据过多 redis.call(ZREMRANGEBYSCORE, key, 0, currentTime - expireTime) -- 6. redis 中添加当前时间 ( 解决多个线程在同一毫秒添加相同 value 导致 Redis 漏记的问题 ) -- 6.1 maxRetries 最大重试次数 retries 重试次数 local maxRetries = 5 local retries = 0 while true do local result = redis.call(ZADD, key, currentTime, currentTime) if result == 1 then -- 6.2 添加成功则跳出循环 break else -- 6.3 未添加成功则 value + 1 再次进行尝试 retries = retries + 1 if retries >= maxRetries then -- 6.4 超过最大尝试次数 采用添加随机数策略 local random_value = math.random(1, 1000) currentTime = currentTime + random_value else currentTime = currentTime + 1 end end end return false1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.

编写 AOP 拦截

复制@Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedisScript<Boolean> limitScript; /** * 限流 * XXX 对限流要求比较高,可以使用在 Redis中对规则进行存储校验 或者使用中间件 * * @param joinPoint joinPoint * @param rateLimiter 限流注解 */ @Before(value = "@annotation(rateLimiter)") public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) { // 1. 生成 key String key = getCombineKey(rateLimiter, joinPoint); try { // 2. 执行脚本返回是否限流 Boolean flag = redisTemplate.execute(limitScript, ListUtil.of(key, String.valueOf(System.currentTimeMillis())), (Object[]) getRules(rateLimiter)); // 3. 判断是否限流 if (Boolean.TRUE.equals(flag)) { log.error("ip: {} 拦截到一个请求 RedisKey: {}", IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()), key); throw new ServiceException(rateLimiter.message());} } catch (ServiceException e) { throw e; } catch (Exception e) { e.printStackTrace(); } } /** * 获取规则 * * @param rateLimiter 获取其中规则信息 * @return */ private Long[] getRules(RateLimiter rateLimiter) { int capacity = rateLimiter.rules().length << 1; // 1. 构建 args Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity]; // 3. 记录数组元素 int index = 0; // 2. 判断是否需要添加防重复提交到redis进行校验 if (rateLimiter.preventDuplicate()) { RateRule preventRateRule = rateLimiter.preventDuplicateRule(); args[index++] = preventRateRule.count(); args[index++] = preventRateRule.timeUnit().toMillis(preventRateRule.time()); } RateRule[] rules = rateLimiter.rules(); for (RateRule rule : rules) { args[index++] = rule.count(); args[index++] = rule.timeUnit().toMillis(rule.time()); } return args; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.

以上,欢迎大家提出意见。

WordPress模板
随机为您推荐
版权声明:本站资源均来自互联网,如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

Copyright © 2016 Powered by Redis 实现多规则限流的思考与实践,益强智未来  滇ICP备2023006006号-17sitemap

回顶部