java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Redis API接口限流

SpringBoot + Redis 实现API接口限流的几种方法

作者:十月初七丶

本文主要介绍了SpringBoot + Redis 实现API接口限流的几种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

了解Redis

Redis(Remote Dictionary Server)是一个开源的高性能键值对存储数据库。它支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。Redis的特点包括:

由于Redis具有高性能、灵活的数据结构和丰富的功能,它被广泛用于缓存、消息队列、计数器、实时排行榜、会话管理等多种应用场景。

需求&为什么需要接口限流

需求:针对相同IP,60s的接口请求次数不能超过10000次

接口限流是为了保护系统和服务,防止因为过多的请求而导致系统过载、性能下降甚至崩溃。以下是进行接口限流的几个主要原因:

综上所述,进行接口限流是保护系统和提升性能的重要手段,对于高并发的系统尤为重要。通过合理设置限流策略,可以有效地平衡资源利用和系统稳定性,提供更好的用户体验。

实现方案

方案一:固定时间段

思路:

当用户在第一次访问该接口时,向Redis中设置一个包含了用户IP和接口方法名的key,value的值初始化为1(表示第一次访问当前接口),同时设置该key的过期时间(60秒),只要此Redis的key没有过期,每次访问都将value的值自增1次,用户每次访问接口前,先从Redis中拿到当前接口访问次数,如果发现访问次数大于规定的次数(超过10000次),则向用户返回接口访问失败的标识。

实现:

(一)拦截器

1、添加Redis依赖:首先在pom.xml文件中添加Spring Data Redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、 配置Redis连接信息:在application.propertiesapplication.yml中配置Redis的连接信息,包括主机、端口、密码等。

3、创建限流拦截器:在项目中创建一个限流拦截器,用于对用户IP进行接口限流。拦截器可以实现HandlerInterceptor接口,并重写preHandle方法进行限流逻辑。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ipAddress = getIpAddress(request);
        String uri = request.getRequestURI().replace("/","_");
        String key = "apiVisits:" + uri + ":" + ipAddress;

        // 判断是否已经达到限流次数
        String value = redisTemplate.opsForValue().get(key);
        // key 不存在,则是第一次请求设置过期时间
        if(StringUtils.isBlank(value)){
            redisTemplate.opsForValue().increment(key, 1);
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
            return true;
        }
        if (value != null && Integer.parseInt(value) > 10) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return false;
        }

        // 未达到限流次数,自增
        redisTemplate.opsForValue().increment(key, 1);
        return true;
    }

    private String getIpAddress(HttpServletRequest request) {
        // 从请求头或代理头中获取真实IP地址
        String ipAddress = request.getHeader("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
        }
        return ipAddress;
    }
}

4、注册拦截器:在配置类中注册自定义的限流拦截器。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
    }
}

(二)AOP

以注解+切面的方式实现,将需要进行限流的API加上注解即可

1、创建注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
    
    /**
     * 缓存key
     */
    String key() default "apiVisits:";

    /**
     * 限流时间,单位秒
     */
    int time() default 5;

    /**
     * 限流次数
     */
    int count() default 10;
}

2、创建AOP切面

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {

    private final RedisTemplate redisTemplate;

    /**
     * 带有注解的方法之前执行
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(currentLimiting)")
    public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
        int time = currentLimiting.time();
        int count = currentLimiting.count();
        // 将接口方法和用户IP构建Redis的key
        String key = getCurrentLimitingKey(currentLimiting.key(), point);
        
        // 判断是否已经达到限流次数
        String value = redisTemplate.opsForValue().get(key);
        if (value != null && Integer.parseInt(value) > count) {
            log.error("接口限流,key:{},count:{},currentCount:{}", key, count, value);
            throw new RuntimeException("访问过于频繁,请稍后再试!");
        }
        // 未达到限流次数,自增
        redisTemplate.opsForValue().increment(key, 1);
        // key 不存在,则是第一次请求设置过期时间
        if(StringUtils.isBlank(value)){
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
    }

    /**
     * 组装 redis 的 key
     */
    private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
        StringBuilder sb = new StringBuilder(prefixKey);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        return sb.append("_").append( targetClass.getName() )
                .append("_").append(method.getName()).toString();
    }
}

缺陷:

当在10:00访问接口,这个时候向Reids写入一条数据访问次数为1,在10:59的时候突然访问了9999次,然后redis过期,在11:00访问了9999次,这样出现的问题就是在10:59到11:00之间访问了9999+9999次。故以固定时间段的方式进行限流可能会不起作用,会存在Reids过期的临界点内造成大量的用户访问。

方案二:滑动窗口

思路:

由于方案一的时间是固定的,我们可以把固定的时间段改成动态的,也就是在用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前一分钟内用户访问该接口的总次数。如果总次数大于限流次数,则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过10000次。

实现:

1、创建注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
    
    /**
     * 缓存key
     */
    String key() default "apiVisits:";

    /**
     * 限流时间,单位秒
     */
    int time() default 5;

    /**
     * 限流次数
     */
    int count() default 10;
}

2、创建AOP切面

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {

    private final RedisTemplate redisTemplate;

    /**
     * 带有注解的方法之前执行
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(currentLimiting)")
    public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
        int time = currentLimiting.time();
        int count = currentLimiting.count();
        // 将接口方法和用户IP构建Redis的key
        String key = getCurrentLimitingKey(currentLimiting.key(), point);
        
        // 使用Zset的 score 设置成用户访问接口的时间戳
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        // 当前时间戳
        long currentTime = System.currentTimeMillis();
        zSetOperations.add(key, currentTime, currentTime);

        // 设置过期时间防止key不消失
        redisTemplate.expire(key, time, TimeUnit.SECONDS);

        // 移除 time 秒之前的访问记录,动态时间段
        zSetOperations.removeRangeByScore(key, 0, currentTime - time * 1000);
        
        // 获得当前时间窗口内的访问记录数
        Long currentCount = zSetOperations.zCard(key);
        // 限流判断
        if (currentCount > count) {
            log.error("接口限流,key:{},count:{},currentCount:{}", key, count, currentCount);
            throw new RuntimeException("访问过于频繁,请稍后再试!");
        }
    }

    /**
     * 组装 redis 的 key
     */
    private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
        StringBuilder sb = new StringBuilder(prefixKey);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        return sb.append("_").append( targetClass.getName() )
                .append("_").append(method.getName()).toString();
    }
}

到此这篇关于SpringBoot + Redis 实现API接口限流的几种方法的文章就介绍到这了,更多相关SpringBoot Redis API接口限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文