redis api限流

限流场景

  1. 秒死活动,恶意使用脚本抢单,需要防止机械进行抢单
  2. 某api 被各式各样系统广泛调用,严重消耗网络,内存等资源,需要合理安排限流
  3. 一下免费的接口提供给用户需要限流和一下测试接口,就需要进行限流了

限流思路

  1. 通过api 路径的作为key ,访问次数为value 的方式对某一用户的某一请求进行唯一标识
  2. 每次访问的时候判断key 是否存在,是否count 超过了限制的访问次数
  3. 落访问次数超出限制,则应response 返回msg 请求过于频繁给前端展示信息

实现

  1. 定义一个aop 注解接口类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
    int seconds();
    int maxCount();
}

  1. aop 加redis 限流
@Aspect
@Component
public class AccessLimitInterceptor{

    @Autowired
    RedisTemplate redisTemplate;


    @Pointcut("@annotation(com.ljh.redis.annotation.AccessLimit)")
    private void cutMethod() {
    }

    @Before("cutMethod()")
    public void begin(JoinPoint point) throws Exception {
        MethodSignature signature = (MethodSignature)point.getSignature();
        //获取次数
        int maxCount = signature.getMethod().getAnnotation(AccessLimit.class).maxCount();
        //获取时间多少秒
        int seconds = signature.getMethod().getAnnotation(AccessLimit.class).seconds();


        //获取request 对象
        ServletRequestAttributes attr = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attr.getRequest();
        HttpServletResponse response = attr.getResponse();
        //限流的key
        String ip = request.getRemoteAddr();
        String key = ip+":8080"+request.getServletPath();

        //获取 访问的次数
        Integer count = (Integer) redisTemplate.boundValueOps(key).get();
        //如果是第一次访问
        if(count==null||count==-1){
            redisTemplate.boundValueOps(key).set(1);
            //设置过期时间
            redisTemplate.boundValueOps(key).expire(Duration.ofSeconds(seconds));
        }
        //如果访问次数<最大次数,加1
        if (count<maxCount){
            redisTemplate.boundValueOps(key).increment();
        } else { //>= 最大次数
            response.getWriter().println("IP 限流中");
            throw new Exception("IP 限流中");
        }
    }
}
### 使用Spring Boot和Redis实现固定时间窗口限流 #### 项目初始化 为了创建一个新的Spring Boot应用程序,可以使用Spring Initializr来快速搭建项目框架。确保引入必要的依赖项,如`spring-boot-starter-data-redis`用于连接Redis数据库以及`spring-boot-starter-aop`支持面向切面编程[^2]。 #### 创建限流注解 定义一个自定义注解`@RateLimit`,该注解可用于标记需要进行速率限制的方法或类。此注解应包含参数以指定允许的最大请求次数及单位时间长度等信息[^1]。 ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int maxRequests() default 100; // 默认最大请求数 String timeWindow(); // 时间窗口大小(秒) } ``` #### Lua脚本编写 准备一段Lua脚本来处理计数逻辑并返回是否超出限额的结果。这段代码将在每次调用受保护的服务端点之前执行,并由Redis服务器解释运行[^4]。 ```lua local key = KEYS[1] local limit = tonumber(ARGV[1]) local current_time = tonumber(redis.call('time')[1]) -- 获取当前存储的时间戳和计数值 local stored_data = redis.call("GET", key) if not stored_data then -- 如果不存在,则设置初始值 redis.call("SET", key, string.format("%d:%d", current_time, 1)) redis.call("EXPIRE", key, ARGV[2]) -- 设置过期时间为窗口周期 else local parts = {} for part in string.gmatch(stored_data, "[^:]+") do table.insert(parts, part) end local last_timestamp = tonumber(parts[1]) local count = tonumber(parts[2]) if (current_time - last_timestamp) >= tonumber(ARGV[2]) then -- 当前时间超过设定间隔,重置计数器 redis.call("SET", key, string.format("%d:%d", current_time, 1)) elseif count < limit then -- 更新现有条目中的计数 redis.call("INCRBY", key, 1) else return "LIMIT_EXCEEDED" end end return "ALLOWED" ``` #### Redis处理器构建 开发服务层组件负责与Redis交互,加载上述Lua脚本并通过命令传递给它相应的参数来进行实际的限流判断工作。 ```java @Service public class RedisRateLimiter { private final ReactiveRedisTemplate<String, Object> template; public RedisRateLimiter(final ReactiveRedisTemplate<String, Object> template){ this.template = template; } /** * 执行Lua脚本实施限流策略. */ public Mono<Boolean> isAllowed(String apiKey, Integer maxRequestsPerSecond, Long windowInSeconds) { List<String> keys = Collections.singletonList(apiKey); List<String> args = Arrays.asList( String.valueOf(maxRequestsPerSecond), String.valueOf(windowInSeconds)); return template.execute(new DefaultRedisScript<>(new Resource() { @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(""" -- 上述Lua脚本内容... """.getBytes()); } }, Boolean.class), keys, args).map(result -> !"LIMIT_EXCEEDED".equals(result)); } } ``` #### 编写AOP切片 利用Spring AOP特性拦截带有特定注解的方法调用,在方法真正被执行之前先检查其是否满足预设条件——即未违反规定的访问频率限制。 ```java @Aspect @Component @Slf4j public class RateLimitingAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimitingAspect.class); private final RedisRateLimiter rateLimiter; public RateLimitingAspect(RedisRateLimiter rateLimiter) { this.rateLimiter = rateLimiter; } @Around("@annotation(rateLimit)") public Object handleApiCall(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); String methodName = method.getName(); Class<?> targetClass = method.getDeclaringClass(); boolean allowed = rateLimiter.isAllowed(targetClass.getSimpleName().concat(".").concat(methodName), rateLimit.maxRequests(), Long.parseLong(rateLimit.timeWindow())).blockOptional().orElse(false); if (!allowed) { throw new TooManyRequestsException("Too many requests"); } else { return joinPoint.proceed(); } } } ``` #### Controller接口测试 最后一步是在控制器中应用新创建好的注解来验证整个流程能否正常运作。当尝试发送过多请求时,应该会收到HTTP状态码429表示太频繁了;而在合理范围内则能成功响应数据。 ```java @RestController @RequestMapping("/api/v1") public class ExampleController { @GetMapping("/test-rate-limit") @RateLimit(timeWindow="60") // 每分钟最多接收100次请求 public ResponseEntity<String> testRateLimit(){ return ResponseEntity.ok("Request processed successfully."); } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值