Redis实现IP限流的2种方式举例详解

 更新时间:2024年08月13日 08:57:48   作者:@猿程序  
通俗的说限流就是限制一段时间内用户访问资源的次数,减轻服务器压力,这篇文章主要给大家介绍了关于Redis实现IP限流的2种方式,文中通过图文介绍的非常详细,需要的朋友可以参考下

通过reids实现

  • 限流的流程图

  • 在配置文件配置限流参数

    blackIP:
      # ip 连续请求的次数
      continue-counts: ${counts:3}
      # ip 判断的时间间隔,单位:秒
      time-interval: ${interval:20}
      # 限制的时间,单位:秒
      limit-time: ${time:30}
    
  • 编写全局过滤器类

    package com.ajie.gateway.filter;
    
    import com.ajie.common.enums.ResponseStatusEnum;
    import com.ajie.common.result.GraceJSONResult;
    import com.ajie.common.utils.CollUtils;
    import com.ajie.common.utils.IPUtil;
    import com.ajie.common.utils.JsonUtils;
    import com.ajie.common.utils.RedisUtil;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.MimeTypeUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Description:
     * @Author: ajie
     */
    @Slf4j
    @Component
    public class IpLimitFilterJwt implements GlobalFilter, Ordered {
    
        @Autowired
        private UrlPathProperties urlPathProperties;
        @Value("${blackIP.continue-counts}")
        private Integer continueCounts;
        @Value("${blackIP.time-interval}")
        private Integer timeInterval;
        @Value("${blackIP.limit-time}")
        private Integer limitTime;
        private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();
            // 3.校验并且排除excludeList
            if (CollUtils.isNotEmpty(ipLimitUrls)) {
                for (String url : ipLimitUrls) {
                    if (antPathMatcher.matchStart(url, path)) {
                        log.warn("IpLimitFilterJwt--url={}", path);
                        // 进行ip限流
                        return doLimit(exchange, chain);
                    }
                }
            }
            // 默认直接放行
            return chain.filter(exchange);
        }
    
        private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 获取真实ip
            ServerHttpRequest request = exchange.getRequest();
            String ip = IPUtil.getIP(request);
    
            /**
             * 需求:
             * 判断ip在20秒内请求的次数是否超过3次
             * 如果超过,则限制访问30秒
             * 等待30秒以后,才能够恢复访问
             */
            // 正常ip
            String ipRedisKey = "gateway_ip:" + ip;
            // 被拦截的黑名单,如果存在,则表示该ip已经被限制访问
            String ipRedisLimitedKey = "gateway_ip:limit:" + ip;
            long limitLeftTime = RedisUtil.KeyOps.getExpire(ipRedisLimitedKey);
            if (limitLeftTime > 0) {
                return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
            }
            // 在redis中获得ip的累加次数
            long requestTimes = RedisUtil.StringOps.incrBy(ipRedisKey, 1);
            // 如果访问次数为1,则表明是第一次访问,在redis设置倒计时
            if (requestTimes == 1) {
                RedisUtil.KeyOps.expire(ipRedisKey, timeInterval, TimeUnit.SECONDS);
            }
    
            // 如果访问次数超过限制的次数,直接将该ip存入限制的redis key,并设置限制访问时间
            if (requestTimes > continueCounts) {
                // 设置该ip需要被限流的时间
                RedisUtil.StringOps.setEx(ipRedisLimitedKey, ip, limitTime, TimeUnit.SECONDS);
                return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
            }
            return chain.filter(exchange);
        }
    
        public Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) {
            // 1.获得response
            ServerHttpResponse response = exchange.getResponse();
            // 2.构建jsonResult
            GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
            // 3.修改response的code为500
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            // 4.设定header类型
            if (!response.getHeaders().containsKey("Content-Type")) {
                response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE);
            }
            // 5.转换json并且向response写入数据
            String jsonStr = JsonUtils.toJsonStr(jsonResult);
            DataBuffer dataBuffer = response.bufferFactory()
                    .wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(dataBuffer));
        }
    
        @Override
        public int getOrder() {
            return 1;
        }
    }
    

通过Lua+Redis实现

业务流程还是和上图差不多,只不过gateway网关不用再频繁和redis进行交互。整个限流逻辑放在redis层,通过Lua代码嵌套

  • Lua实现限流的代码

    --[[
    ipRedisLimitedKey:限流的redis key
    ipRedisKey:未被限流的redis key,通过此key计算访问次数
    timeInterval:访问时间间隔,在此时间内,访问到指定次数进行限流
    limitTime:限流的时长
    ]]
    -- 判断当前ip是否已经被限流
    if redis.call("ttl", ipRedisLimitedKey) > 0 then
        return 1
    end
    
    -- 如果没有被限流,就让当前ip在redis中的值累计1
    local requestTimes = redis.call("incrby", ipRedisKey, 1)
    -- 判断累加后的值
    if requestTimes == 1 then
        -- 如果累加后的值是1,说明是第一次请求,设置一个时间间隔
        redis.call("expire", ipRedisKey, timeInterval)
        return 0
    elseif requestTimes > continueCounts then
        --  如果累加后的值超过了设定的阈值,就对当前ip进行限流
        redis.call("setex", ipRedisLimitedKey, limitTime, ip)
        return 1
    end
    
  • java代码实现Lua和redis的整合

    package com.ajie.gateway.filter;
    
    import com.ajie.common.enums.ResponseStatusEnum;
    import com.ajie.common.result.GraceJSONResult;
    import com.ajie.common.utils.CollUtils;
    import com.ajie.common.utils.IPUtil;
    import com.ajie.common.utils.JsonUtils;
    import com.ajie.common.utils.RedisUtil;
    import com.google.common.collect.Lists;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.MimeTypeUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    
    /**
     * @Description:
     * @Author: ajie
     */
    @Slf4j
    @Component
    public class IpLuaLimitFilterJwt implements GlobalFilter, Ordered {
    
        @Autowired
        private UrlPathProperties urlPathProperties;
        @Value("${blackIP.continue-counts}")
        private Integer continueCounts;
        @Value("${blackIP.time-interval}")
        private Integer timeInterval;
        @Value("${blackIP.limit-time}")
        private Integer limitTime;
        private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();
            // 3.校验并且排除excludeList
            if (CollUtils.isNotEmpty(ipLimitUrls)) {
                for (String url : ipLimitUrls) {
                    if (antPathMatcher.matchStart(url, path)) {
                        log.warn("IpLimitFilterJwt--url={}", path);
                        // 进行ip限流
                        return doLimit(exchange, chain);
                    }
                }
            }
            // 默认直接放行
            return chain.filter(exchange);
        }
    
        private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 获取真实ip
            ServerHttpRequest request = exchange.getRequest();
            String ip = IPUtil.getIP(request);
    
            /**
             * 需求:
             * 判断ip在20秒内请求的次数是否超过3次
             * 如果超过,则限制访问30秒
             * 等待30秒以后,才能够恢复访问
             */
            // 正常ip
            String ipRedisKey = "gateway_ip:" + ip;
            // 被拦截的黑名单,如果存在,则表示该ip已经被限制访问
            String ipRedisLimitedKey = "gateway_ip:limit:" + ip;
            // 通过redis执行lua脚本。返回1代表限流了,返回0代表没有限流
            String script = "if tonumber(redis.call('ttl', KEYS[2])) > 0 then return 1 end local" +
                    " requestTimes = redis.call('incrby', KEYS[1], 1) if tonumber(requestTimes) == 1 then" +
                    " redis.call('expire', KEYS[1], ARGV[2]) return 0 elseif tonumber(requestTimes)" +
                    " > tonumber(ARGV[1]) then redis.call('setex', KEYS[2], ARGV[3], ARGV[4])" +
                    " return 1 else return 0 end";
            Long result = RedisUtil.Helper.execute(script, Long.class,
                    Lists.newArrayList(ipRedisKey, ipRedisLimitedKey),
                    continueCounts, timeInterval, limitTime, ip);
            if(result == 1){
                return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
            }
            return chain.filter(exchange);
        }
    
        public Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) {
            // 1.获得response
            ServerHttpResponse response = exchange.getResponse();
            // 2.构建jsonResult
            GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
            // 3.修改response的code为500
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            // 4.设定header类型
            if (!response.getHeaders().containsKey("Content-Type")) {
                response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE);
            }
            // 5.转换json并且向response写入数据
            String jsonStr = JsonUtils.toJsonStr(jsonResult);
            DataBuffer dataBuffer = response.bufferFactory()
                    .wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(dataBuffer));
        }
    
        @Override
        public int getOrder() {
            return 1;
        }
    }
    

注意事项

  • 在编写lua脚本的时候最好不要一次性写完去试,因为无法进行调试,最好进行拆解。

  • 在进行数字比较时建议加上tonumber()。如果是通过方法传参进来的一定要加,因为redisTemplate默认会把参数当做字符串传入

    如果不转数字就会出现上面的错误

  • 最后也是最重要的,lua代码逻辑一定要对,否则得不到自己想要的结果需要排查很久

总结 

到此这篇关于Redis实现IP限流的2种方式的文章就介绍到这了,更多相关Redis实现IP限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈Redis缓存有哪些淘汰策略

    浅谈Redis缓存有哪些淘汰策略

    redis用做缓存是一种非常常见的手段,然而由于内存大小的限制,会导致redis在内存空间满了以后需要处理继续存入的数据,所以就需要淘汰策略,本文就详细的介绍一下
    2021-08-08
  • Redis cluster集群的介绍

    Redis cluster集群的介绍

    今天小编就为大家分享一篇关于Redis cluster集群的介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Redis中的数据过期策略详解

    Redis中的数据过期策略详解

    这篇文章主要介绍了Redis中的数据过期策略,文中通过示例代码介绍的很详细,相信对大家的理解和学习具有一定的参考借鉴价值,有需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-01-01
  • Redis7.2.x主从复制的实现示例

    Redis7.2.x主从复制的实现示例

    本文主要介绍了Redis7.2.x主从复制的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-06-06
  • Redis安装启动及常见数据类型

    Redis安装启动及常见数据类型

    这篇文章主要介绍了Redis安装启动及常见数据类型,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • Redis实现多级缓存

    Redis实现多级缓存

    这篇文章主要为大家详细介绍了Redis实现多级缓存,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-07-07
  • redis过期监听机制方式

    redis过期监听机制方式

    这篇文章主要介绍了redis过期监听机制方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Redis五种数据结构在JAVA中如何封装使用

    Redis五种数据结构在JAVA中如何封装使用

    本篇博文就针对Redis的五种数据结构以及如何在JAVA中封装使用做一个简单的介绍。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-11-11
  • 详解Redis中的BigKey如何发现和处理

    详解Redis中的BigKey如何发现和处理

    这篇文章主要为大家详细介绍了Redis中的BigKey如何发现和处理,文中给大家详细讲解了BigKey危害和如何解决这些问题,文章通过代码示例和图文介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • redis的持久化和缓存机制解读

    redis的持久化和缓存机制解读

    这篇文章主要介绍了redis的持久化和缓存机制,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06

最新评论