为啥用redis呢(只是此处的使用原因):
因为redis是一个内存数据库,效率高;
redis支持事务;
redis支持分布式,与系统无强关联,不管系统是单机还是分布式部署都支持。
为啥用lua脚本呢:因为lua脚本可以原子性的执行redis命令。
注:千万不要使用网上那种在切面或者拦截器中直接使用redistemplate.opsForValue().get 与set的方式来进行接口请求数量的控制,因为当并发的时候,肯定会出现数量计算不准确的问题。
1.添加pom依赖
<!-- redis引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.yml配置文件:
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
3.redis配置类
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<Object> redisPool() {
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
String maxActive = environment.getProperty("spring.redis.lettuce.pool.max-active");
if(null != maxActive && !"".equals(maxActive)) {
poolConfig.setMaxTotal(Integer.parseInt(maxActive));
}
String maxWait = environment.getProperty("spring.redis.lettuce.pool.max-wait");
if(null != maxWait && !"".equals(maxWait)) {
poolConfig.setMaxWaitMillis(Integer.parseInt(maxWait));
}
String maxIdle = environment.getProperty("spring.redis.lettuce.pool.max-idle");
if(null != maxIdle && !"".equals(maxIdle)) {
poolConfig.setMaxIdle(Integer.parseInt(maxIdle));
}
String minIdle = environment.getProperty("spring.redis.lettuce.pool.min-idle");
if(null != minIdle && !"".equals(minIdle)) {
poolConfig.setMinIdle(Integer.parseInt(minIdle));
}
return new GenericObjectPoolConfig<>();
}
/**
* 配置数据源的
* @author kevin
* @return org.springframework.data.redis.connection.RedisClusterConfiguration
* @date 2022/5/26
*/
@Bean("redisClusterConfig")
@Primary
public RedisClusterConfiguration redisClusterConfig() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
Arrays.asList(nodes().getNodes()));
redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
String maxRedirects = environment.getProperty("spring.redis.cluster.max-redirects");
if(null != maxRedirects && !"".equals(maxRedirects)) {
redisClusterConfiguration.setMaxRedirects(Integer.parseInt(maxRedirects));
}
return redisClusterConfiguration;
}
/**
* 配置数据源的连接工厂
* 这里注意:需要添加@Primary 指定bean的名称,目的是为了创建不同名称的LettuceConnectionFactory
* @author kevin
* @param redisPool :
* @param redisClusterConfig :
* @return org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
* @date 2022/5/26
*/
@Bean("lettuceConnectionFactory")
@Primary
public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig<Object> redisPool,
@Qualifier("redisClusterConfig") RedisClusterConfiguration redisClusterConfig) {
// LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
// .poolConfig(redisPool).build();
LettuceClientConfiguration clientConfiguration = getClientConfiguration(redisPool);
return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
}
/**
* 配置数据源的RedisTemplate
* 注意:这里指定使用名称=factory 的 RedisConnectionFactory
* @author kevin
* @param redisConnectionFactory :
* @return org.springframework.data.redis.core.RedisTemplate
* @date 2022/5/26
*/
@Bean("redisTemplate")
@Primary
public RedisTemplate<String, Object> redisTemplate(@Qualifier("lettuceConnectionFactory")
RedisConnectionFactory redisConnectionFactory) {
return getRedisTemplate(redisConnectionFactory);
}
/**
* lettuce配置信息构建获取--redis集群高可用
* @author kevin
* @param redisPool :
* @return org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
* @date 2022/5/27 12:00
*/
private LettuceClientConfiguration getClientConfiguration(GenericObjectPoolConfig<Object> redisPool) {
//支持自适应集群拓扑刷新和静态刷新源
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
//.enablePeriodicRefresh(Duration.ofSeconds(10)) // 启用定期集群拓扑更新
.enableAllAdaptiveRefreshTriggers() // 自适应拓扑刷新
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10)) // 自适应拓扑更新的超时时间
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder().timeoutOptions(
TimeoutOptions.enabled(Duration.ofSeconds(30))) // 超时修改为30秒
// .autoReconnect(false) //启用或禁用连接丢失时的自动重新连接--默认true
// .pingBeforeActivateConnection(Boolean.TRUE) //在激活连接标志之前设置 PING
// .cancelCommandsOnReconnectFailure(Boolean.TRUE) //允许在重新连接失败的情况下取消排队的命令。默认为 false
// .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) //设置连接处于断开状态时的命令调用行为
.topologyRefreshOptions(clusterTopologyRefreshOptions) //设置ClusterTopologyRefreshOptions拓扑更新的详细控制
.build();
return LettucePoolingClientConfiguration.builder()
.poolConfig(redisPool) //设置GenericObjectPoolConfig驱动程序使用的连接池配置
//.readFrom(ReadFrom.NEAREST) //配置ReadFrom
.clientOptions(clusterClientOptions) //配置ClientOptions
.build();
}
/**
* redisTemplate redis操作工具获取
* @author kevin
* @param factory : redis连接工厂
* @return org.springframework.data.redis.core.RedisTemplate<java.lang.String,java.lang.Object>
* @date 2022/5/27 12:00
*/
private RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL) //过期方法
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisNodes类:
package com.liu.config;
public class RedisNodes {
private String[] nodes;
public String[] getNodes() {
return nodes;
}
public void setNodes(String[] nodes) {
this.nodes = nodes;
}
}
4.编写接口限流redis工具类
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<String, Object> 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<Long> 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);
}
//======================接口限流计数法--处理请求单位时间内接口请求次数 结束========================
}
5.自定义限流注解
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 "请求过于频繁,请稍后再试!";
}
6.编写AOP切面
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);
}
}
7.在需要限流的接口上加自定义的注解
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 : 接口限流测试,处理被限流接口的业务结束!