springboot+redis+lua脚本进行接口限流,解决高并发计数不准确问题
为啥用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 : 接口限流测试,处理被限流接口的业务结束!