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

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

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

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

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.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有序集合实现滑动窗口限流的更多相关文章

  1. 基于redis有序集合,实现简单的延时任务

    基于redis有序集合,实现简单的延时任务 延时任务的场景很多,开发过程中我们经常会遇到,比如说: 1.订单未付款,5分钟后自动取消,这是电商网站非常普遍的需求: 2.用户创建订单不付款,3分钟后自动 ...

  2. ASP.NET Core中使用滑动窗口限流

    滑动窗口算法用于应对请求在时间周期中分布不均匀的情况,能够更精确的应对流量变化,比较著名的应用场景就是TCP协议的流量控制,不过今天要说的是服务限流场景中的应用. 算法原理 这里假设业务需要每秒钟限流 ...

  3. Redis的自增也能实现滑动窗口限流?

    限流是大家开发之路上一定会遇到的需求.比如:限制一定时间内,接口请求请求频率:一定时间内用户发言.评论次数等等,类似于滑动窗口算法.这里分享一份拿来即用的代码,一起看看如何利用常见的 Redis 实现 ...

  4. Redis 有序集合(sorted set)

    Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过分数来为集合中的成员进行从小到大的排序. 有序 ...

  5. redis 有序集合(zset)函数

    redis 有序集合(zset)函数 zAdd 命令/方法/函数 Adds the specified member with a given score to the sorted set stor ...

  6. Redis有序集合

    Redis有序集合类似Redis集合存储在设定值唯一性.不同的是,一个有序集合的每个成员带有分数,用于以便采取有序set命令,从最小的到最大的分数有关. Redis 有序set添加,删除和测试中的O( ...

  7. Redis 有序集合(sorted set),发布订阅,事务,脚本,连接,服务器(三)

    Redis 有序集合(sorted set) Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过 ...

  8. redis有序集合的使用

    Redis 有序集合(sorted set) Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.redis正是通过 ...

  9. redis有序集合数据类型---sortedset

    一.概述 redis有序集合和集合一样,也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数. redis正式通过分数来为集合中的重圆进行从小到大的 ...

  10. 数据结构与算法简记--redis有序集合实现-跳跃表

    跳表 定义 为一个值有序的链表建立多级索引,比如每2个节点提取一个节点到上一级,我们把抽出来的那一级叫做索引或索引层.如下图所示,其中down表示down指针,指向下一级节点.以此类推,对于节点数为n ...

随机推荐

  1. 《使用Gin框架构建分布式应用》阅读笔记:p77-p87

    <用Gin框架构建分布式应用>学习第5天,p77-p87总结,总计11页. 一.技术总结 1.Go知识点 (1)context 2.on-premises software p80, A ...

  2. 使用BackgroundService创建Windows 服务

    使用管理员权限启动cmd.exe 安装服务 sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.Windo ...

  3. 使用 FastGPT 工作流搭建 GitHub Issues 自动总结机器人

    如今任何项目开发节奏都很快,及时掌握项目动态是很重要滴,GitHub Issues 一般都是开发者和用户反馈问题的主要渠道. 然而,随着 Issue 数量的增加,及时跟进每一个问题会变得越来越困难. ...

  4. c++11新增内容

    记录一下c++11新特性方便以后回忆 1.nullptr (对标NULL) 2.auto ,decltype(根据表达式推断类型,表达式不执行) decltype(func()) sum = 5; / ...

  5. Lattice ICE40LP8K开发

    一.开发工具: ICEcube2,界面非常原始,只有PLL IP核添加功能,其他IP核貌似只能使用primitive替换. 不支持时序分析.在线仿真等功能. 二.原语使用 全局布线资源 在 iCE40 ...

  6. Jmeter并发线程场景下共享变量错乱问题解决

    问题复现 问题描述 使用IF控制器获取前一个请求的后置脚本中设置的全局变量->并发线程下通过vars.get获取变量时,第一个线程和第二个线程获取的变量值一样->导致不同基础数据的请求入参 ...

  7. IDEA方法模板、类模板设置记录

    类模板 设置入口 Mac:common+,进入设置->Editor->File and Code Templates->Includes 模版代码 /** * todo * * @a ...

  8. 剖析Air724UG的硬件设计,有大发现?04篇

    ​ 接下来分享第四部分. 5.4 功耗 5.4.1 模块工作电流 测试仪器:综测仪 R&S CMW500,程控电源 安捷伦 66319D 测试条件:VBAT=3.8V,环境温度 25℃,插入白 ...

  9. 【Spring】IOC核心源码学习(二):容器初始化过程

    接上文 啃啃老菜: Spring IOC核心源码学习(一) ,本文将以 ClassPathXmlApplicationContext 这个容器的实现作为基础,学习容器的初始化过程. ClassPath ...

  10. Tornado框架之项目部署(六)

    知识点 supervisor配置与使用 nginx配置 目录: 部署Tornado 1. supervisor 安装 配置 启动 supervisorctl 2. nginx 部署Tornado 为了 ...