• 【框架整合】Redis限流方案


    1、Redis实现限流方案的核心原理:

    redis实现限流的核心原理在于redis 的key 过期时间,当我们设置一个key到redis中时,会将key设置上过期时间,这里的实现是采用lua脚本来实现原子性的。
    
    • 1

    2、准备

    • 引入相关依赖
    <dependency>
    <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    <dependency>
       <groupId>cn.hutoolgroupId>
        <artifactId>hutool-allartifactId>
        <version>5.8.23version>
    dependency>
    <dependency>
       <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
        <version>3.1.5version>
    dependency>
    <dependency>
        <groupId>org.yamlgroupId>
        <artifactId>snakeyamlartifactId>
        <version>2.2version>
    dependency>
    <dependency>
      <groupId>org.apache.commonsgroupId>
        <artifactId>commons-pool2artifactId>
    dependency>
    <dependency>
      <groupId>com.google.protobufgroupId>
        <artifactId>protobuf-javaartifactId>
        <version>3.25.1version>
    dependency>
    
    • 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
    • 添加redis配置信息
    server:
      port: 6650
    
    nosql:
      redis:
        host: XXX.XXX.XXX.XXX
        port: 6379
        password:
        database: 0
    
    spring:
      cache:
        type: redis
      redis:
        host: ${nosql.redis.host}
        port: ${nosql.redis.port}
        password: ${nosql.redis.password}
        lettuce:
          pool:
            enabled: true
            max-active: 8
            max-idle: 8
            min-idle: 0
            max-wait: 1000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 配置redis Conf
    @Configuration
    public class RedisConfig {
    
    
        /**
         * 序列化
         * jackson2JsonRedisSerializer
         *
         * @param redisConnectionFactory 复述,连接工厂
         * @return {@link RedisTemplate}<{@link Object}, {@link Object}>
         */
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
    
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            template.setKeySerializer(jackson2JsonRedisSerializer);
            template.setHashKeySerializer(jackson2JsonRedisSerializer);
            template.setValueSerializer(jackson2JsonRedisSerializer);
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
    
            return template;
        }
        /**
         * 加载lua脚本
         * @return {@link DefaultRedisScript}<{@link Long}>
         */
        @Bean
        public DefaultRedisScript<Long> limitScript() {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luaFile/rateLimit.lua")));
            redisScript.setResultType(Long.class);
            return redisScript;
        }
    
    }
    
    • 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

    3、限流实现

    1. 编写核心lua脚本
    local key = KEYS[1]
    -- 取出key对应的统计,判断统计是否比限制大,如果比限制大,直接返回当前值
    local count = tonumber(ARGV[1])
    local time = tonumber(ARGV[2])
    local current = redis.call('get', key)
    if current and tonumber(current) > count then
        return tonumber(current)
    end
    --如果不比限制大,进行++,重新设置时间
    current = redis.call('incr', key)
    if tonumber(current) == 1 then
        redis.call('expire', key, time)
    end
    return tonumber(current)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 编写注解 limiter
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RateLimiter {
        /**
         * 限流key
         */
        String key() default "rate_limit:";
    
        /**
         * 限流时间,单位秒
         */
        int time() default 60;
    
        /**
         * 限流次数
         */
        int count() default 100;
    
        /**
         * 限流类型
         */
        LimitType limitType() default LimitType.DEFAULT;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    1. 增加注解类型
    public enum LimitType {
        /**
         * 默认策略全局限流
         */
        DEFAULT,
        /**
         * 根据请求者IP进行限流
         */
        IP
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 添加IPUtils
    @Slf4j
    public class IpUtils {
        /**ip的长度值*/
        private static final int IP_LEN = 15;
        /** 使用代理时,多IP分隔符*/
        private static final String SPLIT_STR = ",";
    
        /**
         * 获取IP地址
         * 

    * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 */ public static String getIpAddr(HttpServletRequest request) { String ip = null; try { ip = request.getHeader("x-forwarded-for"); if (StrUtil.isBlank(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StrUtil.isBlank(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { log.error("IPUtils ERROR ", e); } //使用代理,则获取第一个IP地址 if (!StrUtil.isBlank(ip) && ip.length() > IP_LEN) { if (ip.indexOf(SPLIT_STR) > 0) { ip = ip.substring(0, ip.indexOf(SPLIT_STR)); } } return ip; } }

    • 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
    1. 核心处理类
    @Aspect
    @Component
    @Slf4j
    public class RateLimiterAspect {
        @Resource
        private RedisTemplate<Object, Object> redisTemplate;
    
        @Resource
        private RedisScript<Long> limitScript;
    
        @Before("@annotation(rateLimiter)")
        public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
            String key = rateLimiter.key();
            int time = rateLimiter.time();
            int count = rateLimiter.count();
    
            String combineKey = getCombineKey(rateLimiter, point);
            List<Object> keys = Collections.singletonList(combineKey);
            try {
                Long number = redisTemplate.execute(limitScript, keys, count, time);
                if (number == null || number.intValue() > count) {
                    throw new ServiceException("访问过于频繁,请稍候再试");
                }
                log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
            } catch (ServiceException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException("服务器限流异常,请稍候再试");
            }
        }
    
        public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
            StringBuilder stringBuilder = new StringBuilder(rateLimiter.key());
            if (rateLimiter.limitType() == LimitType.IP) {
                stringBuilder.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
            }
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();
            stringBuilder.append(targetClass.getName()).append("-").append(method.getName());
            return stringBuilder.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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    到此,我们就可以利用注解,对请求方法进行限流了

  • 相关阅读:
    后疫情时代新加坡网络安全治理的动态与趋势
    亚马逊差评怎么删?常用的几种删差评方法介绍
    【Rust日报】2022-09-13 Stabilize GAT
    Flink SQL Hudi 实战
    C++模板编程(22)---显式实例化Explicit Instantiation
    pythonopen文件,取出某列,转化成天,然后和当前时间【从1月1日到现在多少天】做比较,得出不可用百分比
    STM32CUBEIDE编译库函数项目及库文件调用
    基于HTML的环境网站设计 HTML+CSS环保网站项目实现 带设计说明psd
    使用xshell linux安装nodejs,CentOS下安装并配置nodejs环境教程
    学习gin-vue-admin之创建api和swagger
  • 原文地址:https://blog.csdn.net/weixin_38996079/article/details/134474198