1.概述
出现自:【Spring】Spring AOP 结合 Redis + Lua 脚本实现分布式限流
此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Redis + Lua 脚本实现分布式限流,旨在保护 API 被恶意频繁访问的问题,是 spring-boot-demo-ratelimit-guava 的升级版。
redis配置文件
redis.hostName=127.0.0.1
redis.port=6379
redis.password=
# 连接超时时间
redis.timeout=10000
#最大空闲数
redis.maxIdle=300
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=1000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000
#连接的最小空闲时间 默认1800000毫秒(30分钟)
redis.minEvictableIdleTimeMillis=300000
#每次释放连接的最大数目,默认3
redis.numTestsPerEvictionRun=1024
#逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
redis.timeBetweenEvictionRunsMillis=30000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
redis.testOnBorrow=true
#在空闲时检查有效性, 默认false
redis.testWhileIdle=true
配置类
package com.spring.config.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.jcache.config.JCacheConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
/***
* todo: 九师兄 2023/4/24 19:43
* 【redis】Spring之RedisTemplate配置与使用
* https://blog.csdn.net/qq_21383435/article/details/130349254
*/
@Configuration
@PropertySource("classpath:redis.properties")
public class RedisConfig extends JCacheConfigurerSupport {
@Autowired
private Environment environment;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory fac = new JedisConnectionFactory();
fac.setHostName(environment.getProperty("redis.hostName"));
fac.setPort(Integer.parseInt(environment.getProperty("redis.port")));
fac.setPassword(environment.getProperty("redis.password"));
fac.setTimeout(Integer.parseInt(environment.getProperty("redis.timeout")));
fac.getPoolConfig().setMaxIdle(Integer.parseInt(environment.getProperty("redis.maxIdle")));
fac.getPoolConfig().setMaxTotal(Integer.parseInt(environment.getProperty("redis.maxTotal")));
fac.getPoolConfig().setMaxWaitMillis(Integer.parseInt(environment.getProperty("redis.maxWaitMillis")));
fac.getPoolConfig().setMinEvictableIdleTimeMillis(
Integer.parseInt(environment.getProperty("redis.minEvictableIdleTimeMillis")));
fac.getPoolConfig()
.setNumTestsPerEvictionRun(Integer.parseInt(environment.getProperty("redis.numTestsPerEvictionRun")));
fac.getPoolConfig().setTimeBetweenEvictionRunsMillis(
Integer.parseInt(environment.getProperty("redis.timeBetweenEvictionRunsMillis")));
fac.getPoolConfig().setTestOnBorrow(Boolean.parseBoolean(environment.getProperty("redis.testOnBorrow")));
fac.getPoolConfig().setTestWhileIdle(Boolean.parseBoolean(environment.getProperty("redis.testWhileIdle")));
return fac;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redis = new RedisTemplate<>();
redis.setConnectionFactory(redisConnectionFactory);
redis.afterPropertiesSet();
return redis;
}
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
}
// 注册脚本相关的东西
@Bean
@SuppressWarnings("unchecked")
public RedisScript<Long> limitRedisScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
切面类RedisRateLimiterAspect
package com.spring.annotation.ratelimiter;
import cn.hutool.core.util.StrUtil;
import com.spring.utils.IpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 限流切面
* </p>
*
* @author yangkai.shen
* @date Created in 2019-09-30 10:30
*
* 加入这个
* @RequiredArgsConstructor(onConstructor_ = @Autowired)
* 如果 com.spring.config.redis.RedisConfig 在有
* @Bean
* public RedisTemplate<String, String> redisTemplate
* 那么会出现
*
* ┌─────┐
* | stringRedisTemplate defined in class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]
* ↑ ↓
* | redisConfig
* └─────┘
*
* 循环依赖
*
* 注释掉这个,然后 private StringRedisTemplate stringRedisTemplate; 这个参数又会空指针异常
*
* 如果注释掉上面的,然后这里加入 也会出现循环依赖
* @Autowired
* private StringRedisTemplate stringRedisTemplate;
*
* 但是这样就不会出现循环依赖了
*
* @Autowired
* @Lazy
* private StringRedisTemplate stringRedisTemplate;
*/
@Slf4j
@Aspect
@Component
//@RequiredArgsConstructor(onConstructor_ = @Autowired) //这个注解是构造方法注解
public class RedisRateLimiterAspect {
private final static String SEPARATOR = ":";
private final static String REDIS_LIMIT_KEY_PREFIX = "limit:";
@Autowired
@Lazy
private StringRedisTemplate stringRedisTemplate;
@Autowired
@Lazy
private RedisScript<Long> limitRedisScript;
@Pointcut("@annotation(com.spring.annotation.ratelimiter.RedisRateLimiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
RedisRateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RedisRateLimiter.class);
if (rateLimiter != null) {
String key = rateLimiter.key();
// 默认用类名+方法名做限流的 key 前缀
if (StrUtil.isBlank(key)) {
key = method.getDeclaringClass().getName() + StrUtil.DOT + method.getName();
}
// 最终限流的 key 为 前缀 + IP地址
// TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理
key = key + SEPARATOR + IpUtil.getIpAddr();
long max = rateLimiter.max();
long timeout = rateLimiter.timeout();
TimeUnit timeUnit = rateLimiter.timeUnit();
boolean limited = shouldLimited(key, max, timeout, timeUnit);
if (limited) {
throw new RuntimeException("手速太快了,慢点儿吧~");
}
}
return point.proceed();
}
private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {
// 最终的 key 格式为:
// limit:自定义key:IP
// limit:类名.方法名:IP
key = REDIS_LIMIT_KEY_PREFIX + key;
// 统一使用单位毫秒
long ttl = timeUnit.toMillis(timeout);
// 当前时间毫秒数
long now = Instant.now().toEpochMilli();
long expired = now - ttl;
// 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String
Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");
if (executeTimes != null) {
if (executeTimes == 0) {
log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max);
return true;
} else {
log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes);
return false;
}
}
return false;
}
}
package com.spring.controller;
import cn.hutool.core.lang.Dict;
import com.spring.annotation.ratelimiter.RedisRateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(path = "/redis/rate/limiter")
public class RedisRateLimiterController {
/***
* todo: 九师兄 2023/5/6 16:50
*
* 界面多次点击
* http://localhost:8082/redis/rate/limiter/test1
* 会出现正常界面和不正常界面
*
* 自定义异常处理!!!
* java.lang.RuntimeException: 手速太快了,慢点儿吧
*
*
*/
@RedisRateLimiter(value = 5)
@GetMapping("/test1")
public Dict test1() {
log.info("【test1】被执行了。。。。。");
return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
}
@GetMapping("/test2")
public Dict test2() {
log.info("【test2】被执行了。。。。。");
return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃");
}
@RedisRateLimiter(value = 2, key = "测试自定义key")
@GetMapping("/test3")
public Dict test3() {
log.info("【test3】被执行了。。。。。");
return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
}
}
这里出现了循环依赖值得注意一下。