基于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 ...
随机推荐
- JDK1.7中关于AutoCloseable接口的使用
1.查看API文档,找到AutoCloseable相关定义 public interface AutoCloseable 该接口是从JDK1.7开始引入,并且该接口中只有一个方法close() 1 v ...
- Ubuntu 22.04 解决和 Windows 共享蓝牙设备的问题
我有一个 Airpods,连接到 WIndows 可以正常工作,但连接到 ubuntu 后会无法连接,只能删除设备选择重联,但是这又会导致 Windows 不能连接到耳机,只能也删除重新连接,费神费力 ...
- 小程序框架开发笔记-wepy
WePY 一.前置 本地使用版本V1.7.3最新版本V2.x 二.使用 export class App extends wepy.app {} 小程序入口,App基类 wepy ...
- 终于注册成功了 Web of Science 账号
地址: https://www.webofscience.com/wos/op/publications/add 个人主页: https://www.webofscience.com/wos/auth ...
- CSP-S2024 游记
CSP-S2024 游记 Day 0 晚上放假回家了,宵夜整了点麦当当,就去睡了. Day 1 本来想多睡会,结果到 \(7:10\) 惊醒了,发现为防止早读迟到已经进化出自动起床功能了. 准备睡回笼 ...
- Struts2漏洞复现
Struts2漏洞复现 靶场环境:/vulhub/struts2大多都是OGNL注入 是什么: Struts2是一个基于MVC设计模式的Web应用框架 识别: 1.通过网页后缀来进行判断,如.do或者 ...
- 用于自然语言处理的循环神经网络RNN
前一篇:<人工智能模型学习到的知识是怎样的一种存在?> 序言:在人工智能领域,卷积神经网络(CNN)备受瞩目,但神经网络的种类远不止于此.实际上,不同类型的神经网络各有其独特的应用场景.在 ...
- ZCMU-1120
就这样 #include<cmath> #include<cstdio> #include<iostream> using namespace std; int m ...
- Conda + JuiceFS :增强 AI 开发环境共享能力
Conda 是当前 AI 应用开发领域中非常流行的环境和包管理系统,因其能够简单便捷地创建与系统资源相隔离的虚拟环境广受欢迎. Conda 支持在不同的操作系统上重建相同的工作环境,但在环境共享复用方 ...
- 『玩转Streamlit』--片段Fragments
在 Streamlit 应用开发中,Fragments组件是一种用于更精细地控制页面元素更新和显示顺序的工具. 它允许开发者将内容分解成多个小的片段,这些片段可以按照特定的顺序或者逻辑进行更新,而不是 ...