基于Redis有序集合实现滑动窗口限流的步骤

2024-12-31 15:50

本文主要是介绍基于Redis有序集合实现滑动窗口限流的步骤,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《基于Redis有序集合实现滑动窗口限流的步骤》滑动窗口算法是一种基于时间窗口的限流算法,通过动态地滑动窗口,可以动态调整限流的速率,Redis有序集合可以用来实现滑动窗口限流,本文介绍基于Redis...

滑动窗口算法是一种基于时间窗口的限流算法,它将时间划分为若干个固定大小的窗口,每个窗口内记录了该时间段内的请求次数。通过动态地滑动窗口,可以动态调整限流的速率,以应对不同的流量变化。

整个限流可以概括为两个主要步骤:

  • 统计窗口内的请求数量
  • 应用限流规则

Redis有序集合每个value有一个score(分数),基于score我们可以定义一个时间窗口,然后每次一个请求进来就设置一个value,这样就可以统计窗口内的请求数量。key可以是资源名,比如一个url,或者ip+url,用户标识+url等。value在这里不那么重要,因为我们只需要统计数量,因此value可以就设置成时间戳,但是如果value相同的话就会被覆盖,所以我们可以把请求的数据做一个hash,将这个hash值当value,或者如果每个请求有流水号的话,可以用请求流水号当value,总之就是要能唯一标识一次请求的。

所以,简化后的命令就变成了:

ZADD  资源标识   时间戳   请求标识

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, String.valueOf(currentTime), currentTime);
    //  设置一个过期时间,及时清理冷数据
    stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
    //  通过
    return true;
}

上面代码中涉及到三条Redis命令,并发请求下可能存在问题,所以我们把它们写成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

完整的代码如下:

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.sjavascriptpringframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import Java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
 * 基于Redis有序集合实现滑动窗口限流
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Service
public class SlidingWindowRatelimiter {
    private long period = 60*1000;  //  1分钟
    private int threshold = 3;      //  3次
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * RedisTemplate
     */
    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, String.valueOf(currentTime), currentTime);
        //  设置一个过期时间,及时清理冷数据
        stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
        //  通过
        return true;
    }
    /**
     * Lua脚本
     */
    public boolean isAllow2(String key) {
        String luaScript = "local key = KEYS[1]\n" +
                "local current_time = tonumberwww.chinasem.cn(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(redisScrphpipt, Collections.singletonList(key), String.valueOf(currentTime), String.valueOf(period), String.valueOf(threshold));
        //  返回1表示通过,返回0表示拒绝
        return "1".equals(result);
    }
}

这里用StringRedisTemplate执行Lua脚本,先把Lua脚本封装成DefaultRedisScript对象。注意,千万注意,Lua脚本的返回值必须是字符串,参数也最好都是字符串,用整型的话可能类型转换错误。

String requestId = UUID.randomUUID().toString();
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
String result = stringRedisTemplate.execute(redisScript,
        Collections.singletonList(key),
        requestId,
        String.valueOf(period),
        String.valueOf(threshold));

好了,上面就是基于Redis有序集合实现的滑动窗口限流。顺带提一句,Redis List类型也可以用来实现滑动窗口。

接下来,我们来完善一下上面的代码,通过AOP来拦截请求达到限流的目的

为此,我们必须自定义注解,然后根据注解参数,来个性化的控制限流。那么,问题来了,如果获取注解参数呢?

举例说明:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnjsotation {
    String value();
}
@ASPect
@Component
public class MyAspect {
    @Before("@annotation(myAnnotation)")
    public void beforeMethod(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        // 获取注解参数
        String value = myAnnotation.value();
        System.out.println("Annotation value: " + value);
        // 其他业务逻辑...
    }
}

注意看,切点是怎么写的 @Before("@annotation(myAnnotation)")

是@Before("@annotation(myAnnotation)"),而不是@Before("@annotation(MyAnnotation)")

myAnnotation,是参数,而MyAnnotation则是注解类

基于Redis有序集合实现滑动窗口限流的步骤

此处参考资料

https://www.cnblogs.com/javaxubo/p/16556924.html

https://blog.csdn.net/qq_40977118/article/details/119488358

https://blog.51cto.com/knifeedge/5529885

言归正传,我们首先定义一个注解

package com.example.demo.controller;
import java.lang.annotation.*;
/**
 * 请求速率限制
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    /**
     * 窗口大小(默认:60秒)
     */
    long period() default 60;
    /**
     * 阈值(默认:3次)
     */
    long threshold() default 3;
}

定义切面

package com.example.demo.controller;
import jakarta.servlet.http.HttpServletRejavascriptquest;
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 org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.util.concurrent.TimeUnit;
/**
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
//    @Autowired
//    private SlidingWindowRatelimiter slidingWindowRatelimiter;
    @Before("@annotation(rateLimit)")
    public void doBefore(JoinPoint joinPoint, RateLimit rateLimit) {
        //  获取注解参数
        long period = rateLimit.period();
        long threshold = rateLimit.threshold();
        //  获取请求信息
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        String uri = httpServletRequest.getRequestURI();
        Long userId = 123L;     //  模拟获取用户ID
        String key = "limit:" + userId + ":" + uri;
        /*
        if (!slidingWindowRatelimiter.isAllow2(key)) {
            log.warn("请求超过速率限制!userId={}, uri={}", userId, uri);
            throw new RuntimeException("请求过于频繁!");
        }*/
        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) {
            //  没有超过阈值,则加入集合
            zSetOperations.add(key, String.valueOf(currentTime), currentTime);
            //  设置一个过期时间,及时清理冷数据
            stringRedisTemplate.expire(key, period, TimeUnit.SECONDS);
        } else {
            throw new RuntimeException("请求过于频繁!");
        }
    }
}

加注解

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

最后,看Redis中的数据结构

基于Redis有序集合实现滑动窗口限流的步骤

最后的最后,流量控制建议看看阿里巴巴 Sentinel

https://sentinelguard.io/zh-cn/

到此这篇关于基于Redis有序集合实现滑动窗口限流的文章就介绍到这了,更多相关基于Redis有序集合实现滑动窗口限流内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于基于Redis有序集合实现滑动窗口限流的步骤的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1152869

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义