基于Redis有序集合实现滑动窗口限流
滑动窗口算法是一种基于时间窗口的限流算法,它将时间划分为若干个固定大小的窗口,每个窗口内记录了该时间段内的请求次数。通过动态地滑动窗口,可以动态调整限流的速率,以应对不同的流量变化。
整个限流可以概括为两个主要步骤:
- 统计窗口内的请求数量
- 应用限流规则
Redis有序集合每个value有一个score(分数),基于score我们可以定义一个时间窗口,然后每次一个请求进来就设置一个value,这样就可以统计窗口内的请求数量。key可以是资源名,比如一个url,或者ip+url,用户标识+url等。value在这里不那么重要,因为我们只需要统计数量,因此value可以就设置成时间戳,但是如果value相同的话就会被覆盖,所以我们可以把请求的数据做一个hash,将这个hash值当value,或者如果每个请求有流水号的话,可以用请求流水号当value,总之就是要能唯一标识一次请求的。
所以,简化后的命令就变成了:
ZADD 资源标识 时间戳 请求标识
Java代码
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.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次
@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 = 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则是注解类

此处参考
https://www.cnblogs.com/javaxubo/p/16556924.html
言归正传,我们首先定义一个注解
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 {
@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中的数据结构

最后的最后,流量控制建议看看阿里巴巴 Sentinel
https://sentinelguard.io/zh-cn/
基于Redis有序集合实现滑动窗口限流的更多相关文章
- 基于redis有序集合,实现简单的延时任务
基于redis有序集合,实现简单的延时任务 延时任务的场景很多,开发过程中我们经常会遇到,比如说: 1.订单未付款,5分钟后自动取消,这是电商网站非常普遍的需求: 2.用户创建订单不付款,3分钟后自动 ...
- ASP.NET Core中使用滑动窗口限流
滑动窗口算法用于应对请求在时间周期中分布不均匀的情况,能够更精确的应对流量变化,比较著名的应用场景就是TCP协议的流量控制,不过今天要说的是服务限流场景中的应用. 算法原理 这里假设业务需要每秒钟限流 ...
- Redis的自增也能实现滑动窗口限流?
限流是大家开发之路上一定会遇到的需求.比如:限制一定时间内,接口请求请求频率:一定时间内用户发言.评论次数等等,类似于滑动窗口算法.这里分享一份拿来即用的代码,一起看看如何利用常见的 Redis 实现 ...
- Redis 有序集合(sorted set)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过分数来为集合中的成员进行从小到大的排序. 有序 ...
- redis 有序集合(zset)函数
redis 有序集合(zset)函数 zAdd 命令/方法/函数 Adds the specified member with a given score to the sorted set stor ...
- Redis有序集合
Redis有序集合类似Redis集合存储在设定值唯一性.不同的是,一个有序集合的每个成员带有分数,用于以便采取有序set命令,从最小的到最大的分数有关. Redis 有序set添加,删除和测试中的O( ...
- Redis 有序集合(sorted set),发布订阅,事务,脚本,连接,服务器(三)
Redis 有序集合(sorted set) Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过 ...
- redis有序集合的使用
Redis 有序集合(sorted set) Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过 ...
- redis有序集合数据类型---sortedset
一.概述 redis有序集合和集合一样,也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数. redis正式通过分数来为集合中的重圆进行从小到大的 ...
- 数据结构与算法简记--redis有序集合实现-跳跃表
跳表 定义 为一个值有序的链表建立多级索引,比如每2个节点提取一个节点到上一级,我们把抽出来的那一级叫做索引或索引层.如下图所示,其中down表示down指针,指向下一级节点.以此类推,对于节点数为n ...
随机推荐
- 解决IDEA中SpringBoot框架使用@Autowired注解方式注入mapper对象出现红色下划线的问题
点击编译器左上角File,点击settings进入设置界面,找到图片中对应的位置,将红线圈住的地方改为Warning,点击右下角的Apply即可
- 哈希(C语言)
文章目录 1.数据结构--哈希表 1.1哈希表的工作原理 1.2哈希表的代码实现 2.哈希算法 2.1 哈希算法介绍 2.2C语言实现示例 本文介绍一个常用的算法--哈希算法,哈希算法依赖于哈希表来实 ...
- Java真的没出路了吗?
Java从1991年由James Gosling和他的同事们开发, 至今已经三十多年, 我们知道,任何产品都有生命周期, 都要经历从诞生.发展.成熟.消亡四个阶段, 目前的Java已经处在成熟阶段, ...
- ubuntu系统安装wps后语言是英文的问题
如果安装ubuntu系统后,在里面安装wps的时候,发现都是英文,无法切换为中文,可以这样操作. 此时点击这里新建一个word空白文档 点击这里新建文档 点击这里显示语言 点击下面的 简体中文 再点击 ...
- vagrant 中 php 7.2 升级到 php 7.3
在本地用 vagrant 给 php 7.2 升级到 7.3 的时候,直接修改了 box/config.yml里面 php 版本 然后就 vagrant reload --provision 进行重新 ...
- 在matlab中使用遗传算法执行最优化
遗传算法是一种通用的最优化方法,具体原理可以看:遗传算法详解与实验.下面记录在Matlab中如何使用遗传算法来做优化. 用法 调用方式如下: 1 x = ga(fun,nvars) 2 x = ga( ...
- pycharm集成Jupyter Notebook
1. Jupyter Notebook Jupyter项目是一个非盈利的开源项目,源于 2014 年的 ipython 项目,支持运行 40 多种编程语言.Jupyter Notebook 的本质是一 ...
- pycharm之远程开发
转载:https://amos-x.com/index.php/amos/archives/pycharm-remote/ 前言 远程开发什么意思? 远程开发就是用本地的个人电脑进行代码编写开发,但是 ...
- 基于.NetCore开发博客项目 StarBlog - (32) 第一期完结
前言 本文记录一下 StarBlog 项目的当前状态与接下来 v2 版本的开发规划. StarBlog 项目从 2022 年开始至今已经 2 年多了,本来早就该给第一期做个小结的,但这种博客类型的项目 ...
- jQuery 元素信息
先贴出元素模型信息 1.获取内容区大小 css():返回值是带单位的(getComputedStyle(node).width) <script> $(function(){ consol ...