基于Redis实现API接口访问次数限制
一,概述
日常开发中会有一个常见的需求,需要限制接口在单位时间内的访问次数,比如说某个免费的接口限制单个IP一分钟内只能访问5次。该怎么实现呢,通常大家都会想到用redis,确实通过redis可以实现这个功能,下面实现一下。
二,常见错误
固定时间窗口
有人设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次。这种设计的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。
三, 实现
1,基于滑动时间窗口
在指定的时间窗口内次数是累积的,超过阈值,都会限制。
2,流程如下
3,代码实现
前提:pom文件引入redis,Spring AOP等
(1)添加注解RequestLimit
package com.xxx.demo.aspect; import java.lang.annotation.*; /** * 接口访问频率注解,默认一分钟只能访问10次 */ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequestLimit { // 限制时间 单位:秒(默认值:一分钟) long period() default 60; // 允许请求的次数(默认值:10次) long count() default 10; }
(2)添加切面实现注解的限制访问逻辑
package com.xxx.demo.aspect; import com.xgd.demo.commons.ErrorCode; import com.xgd.demo.handler.BusinessException; import com.xgd.demo.util.IpUtil; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.log4j.Log4j2; 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.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.concurrent.TimeUnit; /** * @date 2024/11/8 上午8:43 */ @Aspect @Component @Log4j2 public class RequestLimitAspect { @Autowired RedisTemplate redisTemplate; @Pointcut("@annotation(requestLimit)") public void controllerAspect(RequestLimit requestLimit) {} @Around("controllerAspect(requestLimit)") public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable { // 从注解中获取限制次数和窗口时间 long period = requestLimit.period(); long limitCount = requestLimit.count(); // 请求 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); String ip = IpUtil.getIpFromRequest(request); String uri = request.getRequestURI(); //设置客户端访问的key String key = "req_limit_".concat(uri).concat(ip); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); // 添加当前时间戳,分数为当前时间戳 long currentMs = System.currentTimeMillis(); zSetOperations.add(key, currentMs, currentMs); // 设置窗口时间作为过期时间 redisTemplate.expire(key, period, TimeUnit.SECONDS); // 移除掉不在窗口里的数据 zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000); // 查询窗口内已经访问过的次数 Long count = zSetOperations.zCard(key); if (count > limitCount) { log.error("接口拦截:{} 请求超过限制频率【{}次/{}s】,IP为{}", uri, limitCount, period, ip); throw new BusinessException(ErrorCode.REQUEST_LIMITED.getCode(), ErrorCode.REQUEST_LIMITED.getMessage()); } // 继续执行请求 return joinPoint.proceed(); } }
上面里面请求被拦截,是抛出了一个自定义的业务异常,大家可以根据自己的情况自己定义。
(3)同时附上上面中引用到自定义工具类
package com.xxx.demo.util; import jakarta.servlet.http.HttpServletRequest; import java.util.Objects; /** * @date 2024/11/8 上午9:06 */ public class IpUtil { private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; private static final String X_REAL_IP_HEADER = "X-Real-IP"; /** * 从请求中获取IP * * @return IP;当获取不到时,返回null */ public static String getIpFromRequest(HttpServletRequest request ) { return getRealIp(request); } /** * 获取请求的真实IP,优先级从高到低为:<br/> * 1.从请求头X-Forwarded-For中获取ip,并且只获取第一个ip(从左到右) <br/> * 2.从请求头X-Real-IP中获取ip <br/> * 3.使用{@link HttpServletRequest#getRemoteAddr()}方法获取ip * * @param request 请求对象,必须不能为null * @return ip */ private static String getRealIp(HttpServletRequest request) { Objects.requireNonNull(request, "request must be not null"); String ip = request.getHeader(X_FORWARDED_FOR_HEADER); if (ip != null && !ip.isBlank()) { int delimiterIndex = ip.indexOf(','); if (delimiterIndex != -1) { // 如果存在多个ip,则取第一个ip ip = ip.substring(0, delimiterIndex); } return ip; } ip = request.getHeader(X_REAL_IP_HEADER); if (ip != null && !ip.isBlank()) { return ip; } else { return request.getRemoteAddr(); } } }
(4)使用注解
这里限制为10秒内只允许访问3次,超过就抛出异常
(5)访问测试
前3次访问,接口正常访问
后面的访问,返回自定义异常的结果
到此这篇关于基于Redis实现API接口访问次数限制的文章就介绍到这了,更多相关Redis API接口访问限制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
最新评论