springboot+Redis+AOP实现请求限流器

写在开头

本文参考技术帖 程序员那点事

主要对学习经验进行总结,也会加上自己的理解注释。

配置RedisTemplate实例

java 复制代码
//配置redis 使用String数据结构
//对key  value 进行序列化
//根据配置连接redis
@Configuration
public class RedisLimiterHelper {

    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

限流枚举类

java 复制代码
//针对访客和ip进行限流
public enum LimitType {

    /**
     * 自定义key
     */
    CUSTOMER,

    /**
     * 请求者IP
     */
    IP;
}

自定义注解,用于AOP切点

我们自定义个@Limit注解,注解类型为ElementType.METHOD即作用于方法上。

period表示请求限制时间段,count表示在period这个时间段内允许放行请求的次数。limitType代表限流的类型,可以根据请求的IP、自定义key,如果不传limitType属性则默认用方法名作为默认key

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {

    /**
     * 名字
     */
    String name() default "";

    /**
     * key
     */
    String key() default "";

    /**
     * Key的前缀
     */
    String prefix() default "";

    /**
     * 给定的时间范围 单位(秒)
     */
    int period();

    /**
     * 一定时间内最多访问次数
     */
    int count();

    /**
     * 限流的类型(用户自定义key 或者 请求ip)
     */
    LimitType limitType() default LimitType.CUSTOMER;
}

AOP切面

执行主要的限流功能

java 复制代码
@Aspect
@Configuration
public class LimitInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

	//用于ip判断
    private static final String UNKNOWN = "unknown";

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

	//构造注入
    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
        this.limitRedisTemplate = limitRedisTemplate;
    }

    @Pointcut("execution(public * *(..)) && @annotation(com.limit.api.Limit)")
    public void limitPointCut(){} //切点名称,方便通知使用
    
	//环绕通知,执行限流业务
	//@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER) 可根据此注解调用AOP
    @Around("limitPointCut()")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();

        /**
         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
         * IP类型则使用ipAddress,CUSTOMER则使用注解key
         */
        switch (limitType) {
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
		//将key转化为list 方便后续调用lua的keys参数
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
        try {
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            logger.info("Access try count is {} for name={} and key = {}", count, name, key);
            //执行控制器Controller的业务逻辑
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                throw new RuntimeException("You have been dragged into the blacklist");
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                throw new RuntimeException(e.getLocalizedMessage());
            }
            throw new RuntimeException("server exception");
        }
    }

	//lua脚本处理限流逻辑
	//查找key下的访问次数c,若c存在且大于限流值,直接返回
	//key下的c自增1,若key是第一次新建,给过期时间为限流时间
	//返回此次访问后的访问次数c
    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call('get',KEYS[1])");
        // 调用超过了最大值,则直接返回
        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
        lua.append("\nreturn c;");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nc = redis.call('incr',KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流,设置对应键值的过期
        lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }


   	//获取ip地址,通过ip限流
    public String getIpAddress() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

在Controller中可通过注解@Limit的方式调用AOP限流

如一下分析,业务代码省略

java 复制代码
@Limit(key = "limitTest", period = 10, count = 3)
key为方法名,10s请求3次
@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
key为"customer_limit_test",10s请求3次
@Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
key为ipAddress,10s请求3次
相关推荐
ytadpole9 小时前
揭秘xxl-job:从高可用到调度一致性
java·后端
玉衡子9 小时前
六、深入理解JVM执行引擎
java·jvm
每天进步一点_JL10 小时前
JVM 内存调优:到底在调什么?怎么调?
java·jvm·后端
yinke小琪10 小时前
说说Java 中 Object 类的常用的几个方法?详细的讲解一下
java·后端·面试
haciii11 小时前
Spring Boot启动源码深度分析 —— 新手也能看懂的原理剖析
spring boot
间彧13 小时前
Spring Boot项目中如何实现Redis分布式锁
java
掘金安东尼13 小时前
AI 应用落地谈起 ,免费试用 Amazon Bedrock 的最佳时机
java·架构
阿杆13 小时前
为什么我建议你把自建 Redis 迁移到云上进行托管
redis·后端
杨杨杨大侠13 小时前
案例03-附件E-部署运维
java·docker·github
间彧13 小时前
什么是Redis分布式锁,有何使用场景
redis