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

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

  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. OAS常见错误

    body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px } h1, h2 { color: rgba(51, 51, ...

  2. Python 提取PowerPoint文档中的图片

    如果你需要在多个PowerPoint演示文稿中使用相同的图片,直接从原始PPT中提取并保存图片可以避免重复寻找和下载.此外,将PPT中的重要图片提取出来可以将其作为备份,以防原文件损坏或丢失.本文将通 ...

  3. 题解:CF634A Island Puzzle

    CF634A Island Puzzle 题解 分析 由于我们仅能移动 \(0\),所以其它数字的相对顺序较原来应该是不变的,所以我们从环中删除 \(0\) 再判断相对位置即可. 还有需要注意的是本题 ...

  4. 10.Kubernetes核心技术Service

    Kubernetes核心技术Service 前言 前面我们了解到 Deployment 只是保证了支撑服务的微服务Pod的数量,但是没有解决如何访问这些服务的问题.一个Pod只是一个运行服务的实例,随 ...

  5. [这可能是最好的Spring教程!]Maven的模块管理——如何拆分大项目并且用parent继承保证代码的简介性

    问题的提出 在软件开发中,我们为了减少软件的复杂度,是不会把所有的功能都塞进一个模块之中的,塞在一个模块之中对于软件的管理无疑是极其困难且复杂的.所以把一个项目拆分为模块无疑是一个好方法 ┌ ─ ─ ...

  6. P9150 邮箱题

    P9150 邮箱题 Alex_Wei 做法妙. 思路 首先我们可以建出两张图,一张是按照题目的要求形成的有向图,一张是由有向边 \((i,k_i)\) 形成的钥匙图. 在钥匙图中,每个点有且仅有一入度 ...

  7. 使用WebRTC技术搭建小型的视频聊天页面

    目录 目录 参考资料 什么是WebRTC? 能做什么? 架构图 个人理解(类比) 核心知识点 核心知识点类比 ICE框架 STUN(协议) NAT(网络地址转换) TURN SDP(会话描述协议) W ...

  8. (Redis基础教程之七)如何使用Redis中的Hashes

    如何在ubuntu18.04上安装和保护redis 如何连接到Redis数据库 如何管理Redis数据库和Keys 如何在Redis中管理副本和客户端 如何在Redis中管理字符串 如何在Redis中 ...

  9. LNMP一键安装

    PHP环境快捷搭建工具: https://lnmp.org/ [安装] wget https://soft.lnmp.com/lnmp/lnmp2.1.tar.gz -O lnmp2.1.tar.gz ...

  10. Nuxt.js 应用中的 webpack:progress 事件钩子

    title: Nuxt.js 应用中的 webpack:progress 事件钩子 date: 2024/11/27 updated: 2024/11/27 author: cmdragon exce ...