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

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

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

  1. 统计窗口内的请求数量

  2. 应用限流规则

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 thenreturn tostring(0)
elseredis.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.springframework.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次@Autowiredprivate 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 = 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));//  返回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 MyAnnotation {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则是注解类

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

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.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 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 {@Autowiredprivate 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;     //  模拟获取用户IDString 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中的数据结构

文章转载自:废物大师兄

原文链接:https://www.cnblogs.com/cjsblog/p/18638536

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/498512.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++——deque的了解和使用

目录 引言 标准库中的deque 一、deque的基本概念 二、deque的常用接口 1.deque的迭代器 2.deque的初始化 3.deque的容量操作 3.1 有效长度和容量大小 3.2 有效长度和容量操作 4.deque的访问操作 5.deque的修改操作 三、deque的应用场景 结束语 引言 在C中&#x…

【蓝桥杯】:蓝桥杯之路径之谜

题目分析 这是一道路径谜题&#xff0c;描述了一个骑士在一个(n\times n)方格组成的城堡中行走的问题。骑士从西北角&#xff08;入口&#xff09;走到东南角&#xff08;出口&#xff09;&#xff0c;可以横向或纵向移动&#xff0c;但不能斜着走&#xff0c;也不能跳跃。每走…

Mybatis 入门

Mybatis 入门 一、简介 mybatis 是一个优秀的基于 java 的持久层框架&#xff0c;它内部封装了 jdbc&#xff0c;使开发者只需要关注 sql 语句本身&#xff0c; 而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。 mybatis 通过 xml 或注解的方式将要…

《Java核心技术 卷II》流的创建

流的创建 Collection接口中stream方法可以将任何集合转换为一个流。 用静态Stream.of转化成数组。 Stream words Stream.of(contents.split("\\PL")); of方法具有可变长参数&#xff0c;可以构建具有任意数量的流。 使用Array.stream(array,from,to)可以用数组…

uniapp:微信小程序文本长按无法出现复制菜单

一、问题描述 在集成腾讯TUI后&#xff0c;为了能让聊天文本可以复制&#xff0c;对消息组件的样式进行修改&#xff0c;主要是移除下面的user-select属性限制&#xff1a; user-select: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms…

UFS供电

UFS device结构图如上所示&#xff0c;可以看到有三路电源&#xff1a;VCC&#xff0c;VCCQ和VCCQ2。定义如下&#xff1a; 这三路电压参数如下&#xff1a; 上电时序如下所示&#xff1a; 但实际使用的UFS device产品&#xff0c;可能与spce略有不同。我看到的几款三星、美光和…

c++类和对象(六个默认成员函数)

文章目录 一.类的六个默认成员函数二.构造函数1.概念2.特性 三.析构函数1.概念2.特性 四.拷贝构造函数1.概念2.特性 五.赋值操作符重载5.1运算符重载5.2 赋值运算符重载 一.类的六个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。空类中什么都没有吗&#x…

互联网直播点播平台EasyDSS无人机视频推拉流技术实现工地远程监控巡检直播

在建筑行业&#xff0c;施工现场的安全管理和实时监控一直是项目管理中的重点。随着技术的进步&#xff0c;无人机工地直播技术成为了一种新兴的解决方案&#xff0c;它不仅能够提高施工透明度&#xff0c;还能够加强现场安全管理。EasyDSS作为一种先进的流媒体技术平台&#x…

如何使用网络工具进行网络性能评估

网络评估是对IT基础设施的系统评估&#xff0c;以确保它能够很好地满足企业的核心运营需求&#xff0c;确定了基础设施中需要改进的领域&#xff0c;并定义了改进的范围。 网络评估工具分析IT基础设施的各个方面&#xff0c;它通过评估网络设备、网络性能和安全威胁来仔细检查…

【Java项目】基于SpringBoot的【人职匹配推荐系统】

【Java项目】基于SpringBoot的【人职匹配推荐系统】 技术简介&#xff1a;本系统使用采用B/S架构、Spring Boot框架、MYSQL数据库进行开发设计。 系统简介&#xff1a;人职匹配推荐系统分为管理员和用户、企业三个权限子模块。 管理员所能使用的功能主要有&#xff1a;首页、个…

ROS2+OpenCV综合应用--10. AprilTag标签码追踪

1. 简介 apriltag标签码追踪是在apriltag标签码识别的基础上&#xff0c;增加了小车摄像头云台运动的功能&#xff0c;摄像头会保持标签码在视觉中间而运动&#xff0c;根据这一特性&#xff0c;从而实现标签码追踪功能。 2. 启动 2.1 程序启动前的准备 本次apriltag标签码使…

【Vim Masterclass 笔记03】S03L10 + S03L11:Vim 中的文本删除操作以及 Vim 思维习惯的培养(含 DIY 拓展知识点)

文章目录 Section 3&#xff1a;Vim Essentials&#xff08;Vim 核心知识&#xff09;S03L10 Vim 核心浏览命令同步练习点评课S03L11 Deleting Text and "Thinking in Vim" 文本的删除及 Vim 思维习惯的培养1 删除单个字符2 删除一个单词2.1 推广1&#xff1a;D HJK…

【时时三省】(C语言基础)动态内存函数calloc

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 calloc calloc函数也用来动态内存分配 原型如下: void* calloc&#xff08;size&#xff3f;t num, size&#xff3f;t size&#xff09;&#xff1b; 它们两个的区别是 它是需要两个参数…

LeetCode - 初级算法 数组(两个数组的交集 II)

两个数组的交集 II 这篇文章讨论如何求两个数组的交集,并返回结果中每个元素出现的次数与其在两个数组中都出现的次数一致。提供多个实现方法以满足不同场景需求。 免责声明:本文来源于个人知识与公开资料,仅用于学术交流。 描述 给定两个整数数组 nums1 和 nums2,以数…

[react]小技巧, ts如何声明点击事件的类型

很简单, 鼠标放到事件上面就行了 如果想知道点击的是什么元素 ,打印他的nodename就行了 不过得断言为html元素才行 const handleClick (e: React.MouseEvent<HTMLDivElement, MouseEvent>) > {console.log(current, (e.target as HTMLElement).nodeName);}; 为什么…

[创业之路-229]:《华为闭环战略管理》-5-平衡记分卡与战略地图

目录 一、平衡记分卡 1. 财务角度&#xff1a; 2. 客户角度&#xff1a; 3. 内部运营角度&#xff1a; 4. 学习与成长角度&#xff1a; 二、BSC战略地图 1、核心内容 2、绘制目的 3、绘制方法 4、注意事项 一、平衡记分卡 平衡记分卡&#xff08;Balanced Scorecard&…

【中间件】docker+kafka单节点部署---zookeeper模式

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言消息中间件介绍1. KRaft模式2. zookeeper模式2.1. 单节点部署安装验证 前言 最近生产环境上准备部署ELFK日志监控&#xff0c;先在测试环境部署单节点kafka验证…

【mysql】linux安装mysql客户端

参考文章&#xff1a; MySQL系列之如何在Linux只安装客户端 linux下安装mysql客户端client MySQL Community Downloads 查看linux版本方法&#xff1a; lsb_release -a cat /proc/version下载文件&#xff1a; rpm -ivh mysql-community-*可以删除错误的包&#xff1a; RP…

怎么在家访问公司服务器?

在日常工作中&#xff0c;特别是对信息技术从业者而言&#xff0c;工作往往离不开公司的服务器。他们需要定期访问服务器&#xff0c;获取一些关键的机密文件或数据。如果您在家办公&#xff0c;并且需要处理未完成的任务&#xff0c;同时需要从公司服务器获取所需的数据&#…

Unity编译Android apk包进度奇慢或gradle报错的解决方案

最近遇到Unity编译Android apk进度卡在"Calling IPostGenerateGradleAndroidProject callbacks"进度一直不变&#xff0c;如下图&#xff1a; 最后提示编译失败&#xff0c;类似错误如下&#xff1a; Picked up JAVA_TOOL_OPTIONS: -Dfile.encodingUTF-8FAILURE: Bu…