• 基于SpringBoot+Redis实现接口限流


    前言

    业务中需要对一些接口进行限流处理,防止机器人调用或者保证服务质量;

    实现方式

    • 基于redis的lua脚本

    引入依赖

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    redis配置

    package com.qiangesoft.wechat.config;
    
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * redis配置
     *
     * @author qiangesoft
     * @date 2024-03-19
     */
    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Bean
        @SuppressWarnings(value = {"unchecked", "rawtypes"})
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(connectionFactory);
    
            FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
    
            // 使用StringRedisSerializer来序列化和反序列化redis的key值
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(serializer);
    
            // Hash的key也采用StringRedisSerializer的序列化方式
            template.setHashKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(serializer);
    
            template.afterPropertiesSet();
            return template;
        }
    
        @Bean
        public DefaultRedisScript<Long> limitScript() {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(limitScriptText());
            redisScript.setResultType(Long.class);
            return redisScript;
        }
    
        /**
         * 限流脚本
         */
        private String limitScriptText() {
            return "local key = KEYS[1]\n" +
                    "local count = tonumber(ARGV[1])\n" +
                    "local time = tonumber(ARGV[2])\n" +
                    "local current = redis.call('get', key);\n" +
                    "if current and tonumber(current) > count then\n" +
                    "    return tonumber(current);\n" +
                    "end\n" +
                    "current = redis.call('incr', key)\n" +
                    "if tonumber(current) == 1 then\n" +
                    "    redis.call('expire', key, time)\n" +
                    "end\n" +
                    "return tonumber(current);";
        }
    }
    
    
    • 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
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    限流注解

    package com.qiangesoft.wechat.config;
    
    import java.lang.annotation.*;
    
    /**
     * 限流注解
     *
     * @author qiangesoft
     * @date 2024-03-19
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RateLimiter {
    
        /**
         * 限流类型
         */
        LimitType limitType() default LimitType.IP;
    
        /**
         * 限流时间,单位秒
         */
        int time() default 60;
    
        /**
         * 限流次数
         */
        int count() default 10;
    
    }
    
    
    • 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

    限流实现

    package com.qiangesoft.wechat.config;
    
    import com.qiangesoft.wechat.utils.IpUtil;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.Collections;
    import java.util.List;
    
    /**
     * 限流处理
     *
     * @author qiangesoft
     * @date 2024-03-19
     */
    @RequiredArgsConstructor
    @Slf4j
    @Aspect
    @Component
    public class RateLimiterAspect {
    
        private final RedisTemplate<Object, Object> redisTemplate;
    
        private final RedisScript<Long> limitScript;
    
        @Before("@annotation(rateLimiter)")
        public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
            int time = rateLimiter.time();
            int count = rateLimiter.count();
    
            String key = this.buildKey(rateLimiter, point);
            List<Object> keys = Collections.singletonList(key);
            try {
                Long number = redisTemplate.execute(limitScript, keys, count, time);
                if (number == null || number.intValue() > count) {
                    throw new RuntimeException("访问过于频繁,请稍候再试");
                }
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException("服务器限流异常,请稍候再试");
            }
        }
    
        /**
         * 缓存key
         *
         * @param rateLimiter
         * @param point
         * @return
         */
        public String buildKey(RateLimiter rateLimiter, JoinPoint point) {
            String limitId = "";
            if (rateLimiter.limitType() == LimitType.IP) {
                limitId = IpUtil.getIpAddr();
            }
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();
            String key = "rate_limit:" + limitId + "-" + targetClass.getName() + "-" + method.getName();
            return key;
        }
    }
    
    
    • 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
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    测试代码

    package com.qiangesoft.wechat.controller;
    
    import com.qiangesoft.wechat.config.RateLimiter;
    import com.qiangesoft.wechat.utils.ResultVO;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 测试接口
     *
     * @author qiangesoft
     * @date 2024-03-19
     */
    @RestController
    public class TestController {
    
        @RateLimiter
        @GetMapping("/test")
        public ResultVO test() {
            return ResultVO.ok("调用成功!");
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述
    继续频繁点击
    在这里插入图片描述

  • 相关阅读:
    【MySQL】基础操作-DML 与 约束
    IntelliJ IDEA - Raw use of parameterized class
    汽车诊断协议UDS概述
    Elasticsearch基础操作演示总结
    有意思!一个关于 Spring 历史的在线小游戏
    一篇带你搞定⭐《生产环境JVM日志配置》⭐
    Avalonia 11.0.0 正式版发布
    java计算机毕业设计装修设计管理系统设计与实现(附源码、数据库)
    PHP毕业设计源代码剧影评|剧评影评系统
    CSS calc() 使用指南
  • 原文地址:https://blog.csdn.net/weixin_39311781/article/details/136850547