Redis实现IP限流的两种方式详解

gateway网关ip限流

通过reids实现

  1. 限流的流程图

  2. 在配置文件配置限流参数

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

    java 复制代码
    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代码嵌套

  1. 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
  2. java代码实现Lua和redis的整合

    java 复制代码
    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;
        }
    }

注意事项

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

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

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

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

相关推荐
我真的是大笨蛋9 小时前
Redis的String详解
java·数据库·spring boot·redis·spring·缓存
zhengzizhe11 小时前
Redssion出现attempt to unlock lock, not locked by current thread by node id
redis
兜兜风d'15 小时前
redis字符串命令
数据库·redis·缓存
西瓜er16 小时前
Docker 一键部署指南:GitLab、Nacos、Redis、MySQL 与 MinIO 全解析
redis·docker·gitlab
道可到17 小时前
别再瞎拼技术栈!Postgres 已经能干 Redis 的活了
redis·后端·postgresql
野犬寒鸦17 小时前
从零起步学习Redis || 第十二章:Redis Cluster集群如何解决Redis单机模式的性能瓶颈及高可用分布式部署方案详解
java·数据库·redis·后端·缓存
悟能不能悟1 天前
redis的红锁
数据库·redis·缓存
qq_5470261791 天前
SpringBoot+Redis实现电商秒杀方案
spring boot·redis·后端
野犬寒鸦1 天前
从零起步学习Redis || 第十一章:主从切换时的哨兵机制如何实现及项目实战
java·服务器·数据库·redis·后端·缓存
problc2 天前
PostgreSQL + Redis + Elasticsearch 实时同步方案实践:从触发器到高性能搜索
redis·elasticsearch·postgresql