赞
踩
为啥用redis呢(只是此处的使用原因):
因为redis是一个内存数据库,效率高;
redis支持事务;
redis支持分布式,与系统无强关联,不管系统是单机还是分布式部署都支持。
为啥用lua脚本呢:因为lua脚本可以原子性的执行redis命令。
注:千万不要使用网上那种在切面或者拦截器中直接使用redistemplate.opsForValue().get 与set的方式来进行接口请求数量的控制,因为当并发的时候,肯定会出现数量计算不准确的问题。
- <!-- redis引入 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
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<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;
- }
- }
- 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);
- }
- //======================接口限流计数法--处理请求单位时间内接口请求次数 结束========================
-
-
- }

- 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 : 接口限流测试,处理被限流接口的业务结束!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。