springboot整合Redis限流

299 阅读3分钟

思想:

使用redis自增保存接口并发量,然后对并发量校验,控制某段时间内的并发量。

pom 依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.9</version>
</dependency>

boot3.0版本默认配置redis application.properties

# Redis数据库索引(默认为0)
spring.data.redis.database=0  
# Redis服务器地址
spring.data.redis.host=localhost
# Redis服务器连接端口
spring.data.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.data.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.data.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.data.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.data.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.data.redis.lettuce.pool.min-idle=0

Redis工具类

自己定义


import cn.hutool.extra.spring.SpringUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;

/**
* @Classname RedisUtil
* @Author hygge
* @Date 2023/2/11 20:42
* @Description     redis基本命令封装
*/


public class RedisUtil {


    public static StringRedisTemplate getRedisTemplate() {
        return SpringUtil.getBean("stringRedisTemplate");
    }

    /**
    * 根据key获取value
    * @param key   键
    * @return  value
    */
    public static String getString(String key){
        return getRedisTemplate().opsForValue().get(key);
    }

    /**
    * 根据key删除value
    * @param key   键
    */
    public static void delete(String key){
        getRedisTemplate().delete(key);
    }

    /**
    * 根据key,缓存value
    * @param key   键
    * @param value 值
    */
    public static void set(String key, String value) {
        getRedisTemplate().opsForValue().set(key, value);
    }

    /**
    * 根据key,缓存value,有效时间times 毫秒值
    * @param key   键
    * @param value 值
    * @param times 毫秒值
    */
    public static void set(String key, String value, Long times) {
        getRedisTemplate().opsForValue().set(key, value, times, TimeUnit.MILLISECONDS);
    }

    /**
    * 根据key,进行自增+1
    * @param key
    * @return
    */
    public static Long increment(String key){
        return getRedisTemplate().opsForValue().increment(key);
    }

  /**
     * <pre>自增并设置过期key</pre>
     * @param key   
     * @param time  时间秒
     * @param interCurrent  控制并发量
     * @return
     */
    public static boolean checkLimit(String key, Integer time, Integer interCurrent){
        String limitScript = """
            if ( redis.call('exists', KEYS[1]) == 0 ) then 
                   redis.call('incr', KEYS[1])  
                   redis.call('expire', KEYS[1], ARGV[1] ) 
                   return 1  
            else 
                if ( redis.call('incr', KEYS[1]) > tonumber(ARGV[2])) then 
                    return 0 
                end 
                return 1
             end 
                """;
        RedisScript<Long> defaultRedisScript = RedisScript.of(limitScript, Long.class);
        Long result = getStringRedisTemplate().execute(defaultRedisScript, List.of(key), time.toString(), interCurrent.toString());
        return result == 0;
    }

}

编码形式使用


import com.example.hygge.result.R;
import com.example.hygge.utils.RedisUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Classname TestController
 * @Author hygge
 * @Date 2023/3/19 8:53
 * @Description
 */

@RequestMapping("test")
@RestController
public class LimitController {

    private static final Logger log = LoggerFactory.getLogger(LimitController.class);

    /**
     * 限流接口测试
     * @return
     */
    @GetMapping("/limitting")
    public R limitting(HttpServletRequest request){
        if (RedisUtil.checkLimit(request.getRequestURI(), 60, 10)){
            log.info("某段时间内并发大于3进行限流,并发数:{}" , RedisUtil.getString(request.getRequestURI()));
            return R.fail("服务器繁忙,请稍后重试");
        }
        return R.ok();
    }


}

注解形式使用

注解

import java.lang.annotation.*;
​
/**
* @Classname Limitting
* @Author hygge
* @Date 2023/3/19 10:51
* @Description     自定义限流注解
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})  // 注解放置目标位置,METHOD可作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解执行阶段
@Documented // 生成文档
public @interface CurrentLimiting {
​
        /**
        * 默认可以没有,则使用controller uri路径
        */
        String key() default "";
​
        /**
        * 默认并发数量
        */
        int interCurrent() default 10;
​
        /**
        * 限制时间
        */
        int timeSecond() default 30;
​
    }
​

切面类


import cn.hutool.core.util.ObjectUtil;
import com.example.hygge.result.R;
import com.example.hygge.utils.RedisUtil;
import jakarta.servlet.http.HttpServletRequest;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;

/**
 * @Classname CurrentLimiting
 * @Author hygge
 * @Date 2023/3/19 11:02
 * @Description     限流注解注解切面类
 */

@Aspect
@Component
public class CurrentLimiting {

    private static final Logger log = LoggerFactory.getLogger(CurrentLimiting.class);


    /**
     * <pre>定义切点</pre>
     *      Controller层切点 注解拦截
     */
    @Pointcut("@annotation(com.example.hygge.annotation.CurrentLimiting)")
    public void currentLimitings() {

    }


    /**
     * 环绕通知拦截controller
     * @param joinPoint
     */
    @Around(value = "currentLimitings()")
    public Object around(ProceedingJoinPoint joinPoint)  {
        Object resObj = null;
        // 从切面值入点获取植入点方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点方法
        Method method = signature.getMethod();
        // 获取注解
        com.example.hygge.annotation.CurrentLimiting annotation = method.getAnnotation(com.example.hygge.annotation.CurrentLimiting.class);
        // 获取当前请求
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
        // 限流key
        String key = ObjectUtil.isNotEmpty(annotation.key()) ? annotation.key() : request.getRequestURI();

        if (RedisUtil.checkLimit(key, annotation.timeSecond(), annotation.interCurrent())){
            log.info("超出接口承受并发,当前并发量>>>>>>>>{}", RedisUtil.getString(key));
            resObj = R.fail("服务器繁忙,请稍后重试");
        }else {
            // 执行原方法
            try {
                resObj = joinPoint.proceed(joinPoint.getArgs());

            } catch (Throwable e) {
                e.printStackTrace();
                log.error("方法执行异常!");
            }
        }

        return resObj;
    }


测试接口

    /**
     * 注解限流接口测试
     * @return
     */
    @CurrentLimiting(interCurrent = 5)
    @GetMapping("/limit")
    public R limit(){
        log.info("当前并发量正常》》》》》");
        return R.ok();
    }
​