Redis 如何实现限流的,但是大部分都有一个缺点,就是只能实现单一的限流,比如 1 分钟访问 1 次或者 60 分钟访问 10 次这种,
但是如果想一个接口两种规则都需要满足呢,项目又是分布式项目,应该如何解决,下面就介绍一下 Redis 实现分布式多规则限流的方式。
- 如何一分钟只能发送一次验证码,一小时只能发送 10 次验证码等等多种规则的限流;
- 如何防止接口被恶意打击(短时间内大量请求);
- 如何限制接口规定时间内访问次数。
一:使用 String 结构记录固定时间段内某用户 IP 访问某接口的次数
- RedisKey = prefix : className : methodName
- RedisVlue = 访问次数
拦截请求:
- 初次访问时设置 [RedisKey] [RedisValue=1] [规定的过期时间];
- 获取 RedisValue 是否超过规定次数,超过则拦截,未超过则对 RedisKey 进行加1。
规则是每分钟访问 1000 次
- 假设目前 RedisKey => RedisValue 为 999;
- 目前大量请求进行到第一步( 获取 Redis 请求次数 ),那么所有线程都获取到了值为999,进行判断都未超过限定次数则不拦截,导致实际次数超过 1000 次
- 解决办法: 保证方法执行原子性(加锁、Lua)。
考虑在临界值进行访问
二:使用 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
- )
复制代码 ②、注解编写
RateLimiter 注解- @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);
- }
复制代码 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;
- }
复制代码 ③、拦截注解 RateLimiter
- 确定 Redis 存储方式
RedisKey = prefix : className : methodName
RedisScore = 时间戳
RedisValue = 任意分布式不重复的值即可
- 编写生成 RedisKey 的方法
- 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();
- }
复制代码 ④、编写Lua脚本(两种将事件添加到Redis的方法)
Ⅰ: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 false
复制代码 Ⅱ、根据时间戳作为 Zset 中的 value 值
- 参数介绍
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 当前时间
ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 …]
- 根据时间进行生成 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 false
复制代码 ⑤、编写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;
- }
复制代码 到此这篇关于Redis 多规则限流和防重复提交方案实现小结的文章就介绍到这了,更多相关Redis 多规则限流和防重复提交内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/database/335200npz.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |