当前位置: 首页 > article >正文

滑动窗口限流算法:基于Redis有序集合的实现与优化

滑动窗口限流算法是一种基于时间窗口的流量控制策略,它将时间划分为固定大小的窗口,并在每个窗口内记录请求次数。通过动态滑动窗口,算法能够灵活调整限流速率,以应对流量的波动。

算法核心步骤

  1. 统计窗口内的请求数量:记录当前时间窗口内的请求次数。
  2. 应用限流规则:根据预设的阈值判断是否允许当前请求通过。

Redis有序集合的应用

Redis的有序集合(Sorted Set)为滑动窗口限流提供了理想的实现方式。每个有序集合的成员都有一个分数(score),我们可以利用分数来定义时间窗口。每当有请求进入时,将当前时间戳作为分数,并将请求的唯一标识作为成员添加到集合中。这样,通过统计窗口内的成员数量,即可实现限流。

实现细节

Redis命令简化

通过Redis的ZADD命令,我们可以将请求的时间戳和唯一标识添加到有序集合中:

ZADD 资源标识 时间戳 请求标识
Java代码实现

以下是基于Java和Redis的滑动窗口限流实现:

public boolean isAllow(String key) {
    ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
    long currentTime = System.currentTimeMillis();
    long windowStart = currentTime - period;
    zSetOperations.removeRangeByScore(key, 0, windowStart);
    Long count = zSetOperations.zCard(key);
    if (count >= threshold) {
        return false;
    }
    String value = "请求唯一标识(如:请求流水号、哈希值、MD5值等)";
    zSetOperations.add(key, value, currentTime);
    stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
    return true;
}
Lua脚本优化

为了确保在高并发场景下的原子性操作,我们可以将上述逻辑封装为Lua脚本:

local key = KEYS[1]
local current_time = tonumber(ARGV[1])
local window_size = tonumber(ARGV[2])
local threshold = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)
local count = redis.call('ZCARD', key)
if count >= threshold then
    return tostring(0)
else
    redis.call('ZADD', key, tostring(current_time), current_time)
    return tostring(1)
end
完整Java代码
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Service
public class SlidingWindowRatelimiter {
    private long period = 60 * 1000; // 1分钟
    private int threshold = 3; // 3次

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public boolean isAllow(String key) {
        ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - period;
        zSetOperations.removeRangeByScore(key, 0, windowStart);
        Long count = zSetOperations.zCard(key);
        if (count >= threshold) {
            return false;
        }
        String value = "请求唯一标识(如:请求流水号、哈希值、MD5值等)";
        zSetOperations.add(key, value, currentTime);
        stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
        return true;
    }

    public boolean isAllow2(String key) {
        String luaScript = "local key = KEYS[1]\n" +
                "local current_time = tonumber(ARGV[1])\n" +
                "local window_size = tonumber(ARGV[2])\n" +
                "local threshold = tonumber(ARGV[3])\n" +
                "redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)\n" +
                "local count = redis.call('ZCARD', key)\n" +
                "if count >= threshold then\n" +
                " return tostring(0)\n" +
                "else\n" +
                " redis.call('ZADD', key, tostring(current_time), current_time)\n" +
                " return tostring(1)\n" +
                "end";
        long currentTime = System.currentTimeMillis();
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        String result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(currentTime), String.valueOf(period), String.valueOf(threshold));
        return "1".equals(result);
    }
}

AOP实现限流

为了更方便地应用限流策略,我们可以通过AOP(面向切面编程)来拦截请求并应用限流规则。

自定义注解

首先,定义一个限流注解:

package com.example.demo.controller;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    long period() default 60; // 窗口大小(默认:60秒)
    long threshold() default 3; // 阈值(默认:3次)
}
切面实现

然后,实现一个切面来拦截带有@RateLimit注解的方法:

package com.example.demo.controller;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Before("@annotation(rateLimit)")
    public void doBefore(JoinPoint joinPoint, RateLimit rateLimit) {
        long period = rateLimit.period();
        long threshold = rateLimit.threshold();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String uri = httpServletRequest.getRequestURI();
        Long userId = 123L; // 模拟获取用户ID
        String key = "limit:" + userId + ":" + uri;

        ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - period * 1000;
        zSetOperations.removeRangeByScore(key, 0, windowStart);
        Long count = zSetOperations.zCard(key);
        if (count >= threshold) {
            throw new RuntimeException("请求过于频繁!");
        } else {
            zSetOperations.add(key, String.valueOf(currentTime), currentTime);
            stringRedisTemplate.expire(key, period, TimeUnit.SECONDS);
        }
    }
}
使用注解

最后,在需要限流的方法上添加@RateLimit注解:

@RestController
@RequestMapping("/hello")
public class HelloController {
    @RateLimit(period = 30, threshold = 2)
    @GetMapping("/sayHi")
    public void sayHi() {
    }
}

总结

通过Redis有序集合和Lua脚本,我们实现了一个高效且灵活的滑动窗口限流算法。结合AOP,我们可以轻松地将限流策略应用到具体的业务方法中。对于更复杂的流量控制需求,可以参考阿里巴巴的Sentinel框架。

参考链接:

  • Sentinel官方文档
  • AOP实现限流
  • Redis Lua脚本

http://www.kler.cn/a/502020.html

相关文章:

  • OceanBase数据库设计与管理:构建高效分布式数据架构基石
  • 【机器学习】主动学习-增加标签的操作方法-流式选择性采样(Stream-based selective sampling)
  • FFmpeg入门
  • ffmpeg常用命令及介绍
  • 【Linux网络编程】数据链路层 | MAC帧 | ARP协议
  • 使用RSyslog将Nginx Access Log写入Kafka
  • Table-Augmented Generation(TAG):Text2SQL与RAG的升级与超越
  • springboot vue uniapp 仿小红书 1:1 还原 (含源码演示)
  • CVE-2025-22777 (CVSS 9.8):WordPress | GiveWP 插件的严重漏洞
  • 【机器学习】Kaggle实战Rossmann商店销售预测(项目背景、数据介绍/加载/合并、特征工程、构建模型、模型预测)
  • 无源器件-电容
  • Docker 安装开源的IT资产管理系统Snipe-IT
  • 高性能计算服务器是指什么?
  • 洛谷 P3853 [TJOI2007] 路标设置 C语言
  • 企业通过私有安全端点访问大型语言模型的益处
  • RNN之:LSTM 长短期记忆模型-结构-理论详解-及实战(Matlab向)
  • 之前手写的两个好用开源组件优化升级
  • 34_Lua概述与环境安装指南
  • 黑马天机学堂学习计划模块
  • js:根据后端返回数据的最大值进行计算然后设置这个最大值为百分之百,其他的值除这个最大值
  • ThreeJs练习——载入外部模型
  • 【源码解析】Java NIO 包中的 HeapByteBuffer
  • C++—9、如何在Microsoft Visual Studio中调试C++
  • 性能测试工具Jmeter负载模拟
  • TPS61022 PFM的机制以及TPS61xxx转换器的PFM与PWM之间的负载阈值
  • 使用jquery的$.ajax提交带有FormData的post请求报错TypeError: Illegal invocation