当前位置:   article > 正文

SpringBoot + Redis 实现API接口限流_springboot redis接口限流

springboot redis接口限流

目录

了解Redis

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

实现方案

方案一:固定时间段

思路:

实现:

(一)拦截器

 (二)AOP

缺陷:

方案二:滑动窗口

思路:

实现:


了解Redis

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

  1. 内存存储:Redis将数据存储在内存中,因此读写速度非常快,适用于对性能有较高要求的场景。

  2. 持久化:Redis支持持久化将内存中的数据保存到硬盘上,以便在服务器重启后能够恢复数据。

  3. 数据结构多样:Redis不仅仅支持简单的键值对存储,还支持丰富的数据结构,例如列表、集合、有序集合等,使其具备更多的功能和用途。

  4. 高并发:Redis是单线程模型,通过使用异步I/O和非阻塞I/O来支持高并发。

  5. 多语言支持:Redis支持多种编程语言的客户端,如Java、Python、C#等,便于开发人员在不同平台上使用。

  6. 发布/订阅:Redis支持发布/订阅模式,允许客户端订阅一个或多个频道并接收对应频道的消息。

  7. 事务支持:Redis支持事务,可以在一个事务中执行多个命令,并保证这些命令的原子性。

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

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

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

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

  1. 防止恶意攻击:接口限流可以防止恶意用户或者攻击者通过大量的请求来攻击系统,保护系统的稳定性和安全性。

  2. 保护系统资源:对于一些计算密集型或者资源消耗较大的接口,限制请求的频率可以避免服务器资源被过度消耗,保障其他正常请求的处理。

  3. 避免雪崩效应:当某个服务不可用或者响应时间过长时,如果没有限流措施,大量请求可能会涌入后端,导致更多的请求失败,产生雪崩效应。

  4. 提升系统性能:限流可以控制并发请求数,避免过多的请求导致服务器负载过高,从而提升系统的整体性能和响应速度。

  5. 提供公平资源分配:通过限流,可以实现对不同用户或者不同服务请求的公平分配,避免某些请求占用过多资源而影响其他请求。

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

实现方案

方案一:固定时间段

思路:

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

实现:

(一)拦截器

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

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

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

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

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.data.redis.core.RedisTemplate;
  3. import org.springframework.web.servlet.HandlerInterceptor;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.util.concurrent.TimeUnit;
  7. public class RateLimitInterceptor implements HandlerInterceptor {
  8. @Autowired
  9. private RedisTemplate<String, String> redisTemplate;
  10. @Override
  11. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  12. String ipAddress = getIpAddress(request);
  13. String uri = request.getRequestURI().replace("/","_");
  14. String key = "apiVisits:" + uri + ":" + ipAddress;
  15. // 判断是否已经达到限流次数
  16. String value = redisTemplate.opsForValue().get(key);
  17. // key 不存在,则是第一次请求设置过期时间
  18. if(StringUtils.isBlank(value)){
  19. redisTemplate.opsForValue().increment(key, 1);
  20. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  21. return true;
  22. }
  23. if (value != null && Integer.parseInt(value) > 10) {
  24. response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
  25. return false;
  26. }
  27. // 未达到限流次数,自增
  28. redisTemplate.opsForValue().increment(key, 1);
  29. return true;
  30. }
  31. private String getIpAddress(HttpServletRequest request) {
  32. // 从请求头或代理头中获取真实IP地址
  33. String ipAddress = request.getHeader("X-Forwarded-For");
  34. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  35. ipAddress = request.getHeader("Proxy-Client-IP");
  36. }
  37. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  38. ipAddress = request.getHeader("WL-Proxy-Client-IP");
  39. }
  40. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  41. ipAddress = request.getRemoteAddr();
  42. }
  43. return ipAddress;
  44. }
  45. }

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

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  3. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  4. @Configuration
  5. public class WebMvcConfig implements WebMvcConfigurer {
  6. @Autowired
  7. private RateLimitInterceptor rateLimitInterceptor;
  8. @Override
  9. public void addInterceptors(InterceptorRegistry registry) {
  10. registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
  11. }
  12. }
 (二)AOP

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

1、创建注解

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface CurrentLimiting {
  4. /**
  5. * 缓存key
  6. */
  7. String key() default "apiVisits:";
  8. /**
  9. * 限流时间,单位秒
  10. */
  11. int time() default 5;
  12. /**
  13. * 限流次数
  14. */
  15. int count() default 10;
  16. }

2、创建AOP切面

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. @RequiredArgsConstructor
  5. public class CurrentLimitingAspect {
  6. private final RedisTemplate redisTemplate;
  7. /**
  8. * 带有注解的方法之前执行
  9. */
  10. @SuppressWarnings("unchecked")
  11. @Before("@annotation(currentLimiting)")
  12. public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
  13. int time = currentLimiting.time();
  14. int count = currentLimiting.count();
  15. // 将接口方法和用户IP构建Redis的key
  16. String key = getCurrentLimitingKey(currentLimiting.key(), point);
  17. // 判断是否已经达到限流次数
  18. String value = redisTemplate.opsForValue().get(key);
  19. if (value != null && Integer.parseInt(value) > count) {
  20. log.error("接口限流,key:{},count:{},currentCount:{}", key, count, value);
  21. throw new RuntimeException("访问过于频繁,请稍后再试!");
  22. }
  23. // 未达到限流次数,自增
  24. redisTemplate.opsForValue().increment(key, 1);
  25. // key 不存在,则是第一次请求设置过期时间
  26. if(StringUtils.isBlank(value)){
  27. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  28. }
  29. }
  30. /**
  31. * 组装 redis 的 key
  32. */
  33. private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
  34. StringBuilder sb = new StringBuilder(prefixKey);
  35. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  36. HttpServletRequest request = attributes.getRequest();
  37. sb.append( Utils.getIpAddress(request) );
  38. MethodSignature signature = (MethodSignature) point.getSignature();
  39. Method method = signature.getMethod();
  40. Class<?> targetClass = method.getDeclaringClass();
  41. return sb.append("_").append( targetClass.getName() )
  42. .append("_").append(method.getName()).toString();
  43. }
  44. }

缺陷:

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

方案二:滑动窗口

思路:

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

实现:

1、创建注解

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface CurrentLimiting {
  4. /**
  5. * 缓存key
  6. */
  7. String key() default "apiVisits:";
  8. /**
  9. * 限流时间,单位秒
  10. */
  11. int time() default 5;
  12. /**
  13. * 限流次数
  14. */
  15. int count() default 10;
  16. }

2、创建AOP切面

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. @RequiredArgsConstructor
  5. public class CurrentLimitingAspect {
  6. private final RedisTemplate redisTemplate;
  7. /**
  8. * 带有注解的方法之前执行
  9. */
  10. @SuppressWarnings("unchecked")
  11. @Before("@annotation(currentLimiting)")
  12. public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
  13. int time = currentLimiting.time();
  14. int count = currentLimiting.count();
  15. // 将接口方法和用户IP构建Redis的key
  16. String key = getCurrentLimitingKey(currentLimiting.key(), point);
  17. // 使用Zset的 score 设置成用户访问接口的时间戳
  18. ZSetOperations zSetOperations = redisTemplate.opsForZSet();
  19. // 当前时间戳
  20. long currentTime = System.currentTimeMillis();
  21. zSetOperations.add(key, currentTime, currentTime);
  22. // 设置过期时间防止key不消失
  23. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  24. // 移除 time 秒之前的访问记录,动态时间段
  25. zSetOperations.removeRangeByScore(key, 0, currentTime - time * 1000);
  26. // 获得当前时间窗口内的访问记录数
  27. Long currentCount = zSetOperations.zCard(key);
  28. // 限流判断
  29. if (currentCount > count) {
  30. log.error("接口限流,key:{},count:{},currentCount:{}", key, count, currentCount);
  31. throw new RuntimeException("访问过于频繁,请稍后再试!");
  32. }
  33. }
  34. /**
  35. * 组装 redis 的 key
  36. */
  37. private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
  38. StringBuilder sb = new StringBuilder(prefixKey);
  39. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  40. HttpServletRequest request = attributes.getRequest();
  41. sb.append( Utils.getIpAddress(request) );
  42. MethodSignature signature = (MethodSignature) point.getSignature();
  43. Method method = signature.getMethod();
  44. Class<?> targetClass = method.getDeclaringClass();
  45. return sb.append("_").append( targetClass.getName() )
  46. .append("_").append(method.getName()).toString();
  47. }
  48. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/454983
推荐阅读
相关标签
  

闽ICP备14008679号