项目实现:当一个请求在5秒内请求超过5次,则抛出异常
项目结构
父项目POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jane</groupId>
<artifactId>spring-redis</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>redis-tool</module>
<module>eureka-client</module>
</modules>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>
子项目redis-tool
端口:8080
-
POM文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-redis</artifactId>
<groupId>com.jane</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>redis-tool</artifactId>
</project>
-
lua文件
tonumber是指将字符串转为数字;redis.call是用来引用方法,如get incrby(自增1) expire(到期)
local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"2")
return current + 1
end
-
config类
1、读取lua、读取请求数、限定的时间
@Component
public class Commons {
@Bean
public DefaultRedisScript<Number> redisluaScript() {
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
//读取 lua 脚本
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
redisScript.setResultType(Number.class);
return redisScript;
}
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
2、限流注解
默认key叫limit 时间为5秒内,请求数不能超过5
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "limit";
int time() default 5;
int count() default 5;
}
3、 拦截器,当有@RateLimit注解时,进行拦截
(1)判断是否遇到@RateLimit注解:RateLimit rateLimit = method.getAnnotation(RateLimit.class); rateLimit不为空
(2)@Around("execution(* com.jane.controller ..*(..) )") //execution后面填的是controller所在的包名
(3)Collections.singletonList是指生成一个不重复的list,
(4)limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
执行lua脚本
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args)
@Aspect
@Configuration
public class LimitAspect {
private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);
@Autowired
private RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
private DefaultRedisScript<Number> redisluaScript;
@Around("execution(* com.jane.controller ..*(..) )")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit != null) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ipAddress = getIpAddr(request);
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(ipAddress).append("-")
.append(targetClass.getName()).append("- ")
.append(method.getName()).append("-")
.append(rateLimit.key());
List<String> keys = Collections.singletonList(stringBuffer.toString());
Number number = limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
logger.info("限流时间段内访问第:{} 次", number.toString());
return joinPoint.proceed();
}
} else {
return joinPoint.proceed();
}
//由于本文没有配置公共异常类,如果配置可替换
throw new RuntimeException("已经到设置限流次数");
}
private static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}
子项目eureka-client
引入依赖:
<dependencies>
<dependency>
<groupId>com.jane</groupId>
<artifactId>redis-tool</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
-
application.yml
server:
port: 8081
spring:
# Redis数据库索引
redis:
database: 0
# Redis服务器地址
host: xxxxx
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接池最大连接数(使用负值表示没有限制)
jedis:
pool:
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 10000
eureka:
client:
register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
fetch-registry: false # 是否拉取其它服务的信息,默认是true
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://xxxx:xxx
-
controller
引用@RateLimit注解,设定time count key
@RestController
public class LimitController {
@RateLimit(key = "test", time = 10, count = 10)
@GetMapping("/test/limit")
public String testLimit() {
return "Hello,ok";
}
@RateLimit()
@GetMapping("/test/limit/a")
public String testLimitA() {
return "Hello,ok";
}
}
还没有整合eureka,后续整合后会更新