为啥用redis呢(只是此处的使用原因):
因为redis是一个内存数据库,效率高;
redis支持事务;
redis支持分布式,与系统无强关联,不管系统是单机还是分布式部署都支持。
为啥用lua脚本呢:因为lua脚本可以原子性的执行redis命令。
注:千万不要使用网上那种在切面或者拦截器中直接使用redistemplate.opsForValue().get 与set的方式来进行接口请求数量的控制,因为当并发的时候,肯定会出现数量计算不准确的问题。
org.springframework.boot
spring-boot-starter-data-redis
spring: redis: #密码 password: kevin #连接超时时长(毫秒) timeout: 30000 cluster: #集群节点以逗号分隔,或换行后 - 开头 nodes: - 127.0.0.1:6381 - 127.0.0.1:6382 - 127.0.0.1:6383 - 127.0.0.1:6384 - 127.0.0.1:6385 - 127.0.0.1:6386 # 获取失败 最大重定向次数 max-redirects: 3 #lettuce连接池信息 # 连接池最大连接数(使用负值表示没有限制) 默认为8 lettuce: pool: # 连接池最大连接数 max-active: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1 max-wait: -1 # 连接池中的最大空闲连接 默认为8 max-idle: 200 # 连接池中的最小空闲连接 默认为 0 min-idle: 100
package com.liu.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.liu.redisexpired.RedisMessageListenerFactory;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Arrays;
@Configuration
public class RedisConfig {
@Autowired
private Environment environment;
@Bean(value = "nodes")
@ConfigurationProperties(prefix = "spring.redis.cluster")
public RedisNodes nodes(){
return new RedisNodes();
}
/**
* 配置lettuce连接池
* @author kevin
* @return org.apache.commons.pool2.impl.GenericObjectPoolConfig
* @date 2022/5/26
*/
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.redis.cluster.lettuce.pool")
public GenericObjectPoolConfig
RedisNodes类:
package com.liu.config;
public class RedisNodes {
private String[] nodes;
public String[] getNodes() {
return nodes;
}
public void setNodes(String[] nodes) {
this.nodes = nodes;
}
}
package com.liu.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.GenericToStringSerializer;
import org.springframework.stereotype.Component;
import java.util.Collections;
/**
* redis工具类
*
* @author kevin
* @date 2021/3/30
*/
@Slf4j
@Component
@SuppressWarnings({"unused"})
public class RedisUtils {
private static final Long SUCCESS = 1L;
/*
* 注入redisTemplate bean(可配置不同的template secondaryRedisTemplate)
*/
@Autowired
@Qualifier(value = "redisTemplate") //指定注入的template模版
private RedisTemplate redisTemplate;
//======================接口限流计数法--处理请求单位时间内接口请求次数 开始========================
/**
* 判断单位时间内请求数量是否超过限制--使用lua脚本执行,保证原子性
*
* @param key : 键
* @param time : 单位时间(秒)
* @param count : 最大请求次数
* @return boolean true成功 false失败
* @author kevin
* @date 2022/5/31
*/
public boolean requestLimit(String key, int time, int count) {
//lua 脚本,进行请求次数的叠加,并判断请求次数是否超过限制
String script = "local val = redis.call('incr', KEYS[1]) " +
"local expire = tonumber(ARGV[1]) " +
"if val == 1 " +
"then redis.call('expire', KEYS[1], expire) " +
"else if redis.call('ttl', KEYS[1]) == -1 " +
"then redis.call('expire', KEYS[1], expire) " +
"end " +
"end " +
"if val > tonumber(ARGV[2]) " +
"then return 0 " +
"end " +
"return 1";
RedisScript redisScript = new DefaultRedisScript<>(script, Long.class);
// execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
// 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
// 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
new GenericToStringSerializer<>(Long.class), Collections.singletonList(key), time, count);
return SUCCESS.equals(result);
}
//======================接口限流计数法--处理请求单位时间内接口请求次数 结束========================
}
package com.liu.requestlimit;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 限制单位时间内,接口被请求的次数
* @author kevin
* @date 2022/5/26
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {
/*
限流的key
*/
String key() default "request_limit_";
/*
单位时间
*/
int time() default 60;
/*
限流时间单位,默认秒
*/
TimeUnit timeunit() default TimeUnit.SECONDS;
/*
单位时间内可访问次数
*/
int count() default 100;
/*
限流的类型:全局,IP
*/
RequestLimitType limitType() default RequestLimitType.DEFAULT;
String msg() default "请求过于频繁,请稍后再试!";
}
package com.liu.requestlimit;
import com.liu.utils.RedisUtils;
import com.liu.utils.WebUtils;
import com.liu.vo.ResponseVo;
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.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.StringJoiner;
@Slf4j
@Aspect
@Component
public class RequestLimitAspect {
@Resource
private RedisUtils redisUtils;
@Pointcut("@annotation(requestLimit)")
public void doBefore(RequestLimit requestLimit){
//切点定义
}
@Around(value = "doBefore(requestLimit)", argNames = "joinPoint,requestLimit")
public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(null == requestAttributes){
log.info("限流失败,未获取到请求request");
return joinPoint.proceed();
}
HttpServletRequest request = requestAttributes.getRequest();
int time = requestLimit.time();
int maxCount = requestLimit.count();
StringJoiner key = new StringJoiner("");
key.add(requestLimit.key());
if(RequestLimitType.IP.code == requestLimit.limitType().code){
key.add(WebUtils.getIP(request) + ":");
}
key.add(request.getRequestURI());
String keyStr = key.toString();
// 使用lua脚本执行,保证原子性,进行请求次数限制
boolean isOutOfLimit = redisUtils.requestLimit(keyStr, time, maxCount);
if(!isOutOfLimit){
HttpServletResponse response = requestAttributes.getResponse();
responseFailed(response, requestLimit.msg());
return null;
}
return joinPoint.proceed();
}
private void responseFailed(HttpServletResponse response, String msg){
ResponseVo result = new ResponseVo.Builder().error().message(msg).build();
log.info(msg);
WebUtils.responseOutJson(response, result);
}
}
package com.liu.controller;
import com.liu.requestlimit.RequestLimit;
import com.liu.requestlimit.RequestLimitType;
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("/test")
public class TestController {
/**
* 测试接口限流
*
* @author kevin
* @date 2022/6/2 10:42
*/
@RequestLimit(time = 10, count = 5, limitType = RequestLimitType.IP)
@GetMapping("/testLimitRequest")
public void testLimitRequest() throws InterruptedException {
log.info("接口限流测试,开始处理被限流接口的业务......");
Thread.sleep(2000);
log.info("接口限流测试,处理被限流接口的业务结束!");
}
}
限流类型枚举类--RequestLimitType:
package com.liu.requestlimit;
@SuppressWarnings({"unused"})
public enum RequestLimitType {
DEFAULT(1, "全局限制"),
IP(2, "通过IP限制");
int code;
String desc;
RequestLimitType(int code, String desc){
this.code = code;
this.desc = desc;
}
/**
* 通过代码值获取枚举
* @author kevin
* @param code :
* @return com.cetccloud.base.enums.RequestLimitType
* @date 2020/12/23 16:09
*/
public static RequestLimitType getByCode(int code){
for(RequestLimitType c : RequestLimitType.values()){
if(code == c.getCode()){
return c;
}
}
return null;
}
/**
* 通过描述获取枚举
* @author kevin
* @param desc :
* @return com.cetccloud.base.enums.RequestLimitType
* @date 2022/5/26
*/
public static RequestLimitType getByDesc(String desc){
for(RequestLimitType c : RequestLimitType.values()){
if(desc.equals(c.getDesc())){
return c;
}
}
return null;
}
/**
* 通过代码值获得代码描述
* @author kevin
* @param code :
* @return java.lang.String
* @date 2022/5/26
*/
public static String getDesc(int code){
for(RequestLimitType c : RequestLimitType.values()){
if(code == c.getCode()){
return c.desc;
}
}
return null;
}
/**
* 通过代码描述获得代码值
* @author kevin
* @param desc :
* @return int
* @date 2022/5/26
*/
public static int getCode(String desc){
for(RequestLimitType c : RequestLimitType.values()){
if(desc.equals(c.getDesc())){
return c.code;
}
}
return -99;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
8.直接jmeter测试并发
2022-06-06 14:55:53.430 INFO 13428 --- [nio-8081-exec-2] com.liu.controller.TestController : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430 INFO 13428 --- [nio-8081-exec-4] com.liu.controller.TestController : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430 INFO 13428 --- [nio-8081-exec-3] com.liu.controller.TestController : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430 INFO 13428 --- [nio-8081-exec-1] com.liu.controller.TestController : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430 INFO 13428 --- [nio-8081-exec-6] com.liu.controller.TestController : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.555 INFO 13428 --- [nio-8081-exec-5] com.liu.requestlimit.RequestLimitAspect : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555 INFO 13428 --- [nio-8081-exec-7] com.liu.requestlimit.RequestLimitAspect : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555 INFO 13428 --- [nio-8081-exec-9] com.liu.requestlimit.RequestLimitAspect : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555 INFO 13428 --- [nio-8081-exec-8] com.liu.requestlimit.RequestLimitAspect : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.648 INFO 13428 --- [io-8081-exec-10] com.liu.requestlimit.RequestLimitAspect : 请求过于频繁,请稍后再试!
2022-06-06 14:55:55.445 INFO 13428 --- [nio-8081-exec-6] com.liu.controller.TestController : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445 INFO 13428 --- [nio-8081-exec-2] com.liu.controller.TestController : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445 INFO 13428 --- [nio-8081-exec-1] com.liu.controller.TestController : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445 INFO 13428 --- [nio-8081-exec-4] com.liu.controller.TestController : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445 INFO 13428 --- [nio-8081-exec-3] com.liu.controller.TestController : 接口限流测试,处理被限流接口的业务结束!