
为啥用redis呢只是此处的使用原因因为redis是一个内存数据库效率高redis支持事务redis支持分布式与系统无强关联不管系统是单机还是分布式部署都支持。为啥用lua脚本呢因为lua脚本可以原子性的执行redis命令。注千万不要使用网上那种在切面或者拦截器中直接使用redistemplate.opsForValue().get 与set的方式来进行接口请求数量的控制因为当并发的时候肯定会出现数量计算不准确的问题。1.添加pom依赖!-- redis引入 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency2.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: 1003.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 GenericObjectPoolConfigObject redisPool() { GenericObjectPoolConfigObject 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(GenericObjectPoolConfigObject 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 RedisTemplateString, 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(GenericObjectPoolConfigObject 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.RedisTemplatejava.lang.String,java.lang.Object * date 2022/5/27 12:00 */ private RedisTemplateString, Object getRedisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializerObject 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 RedisTemplateString, 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; RedisScriptLong 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(接口限流测试处理被限流接口的业务结束); } }限流类型枚举类--RequestLimitTypepackage 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 : 接口限流测试处理被限流接口的业务结束