当前位置: 首页 > news >正文

Redis实现滑动窗口限流

常见限流算法

  1. 固定窗口算法

    在固定的时间窗口下进行计数,达到阈值就拒绝请求。固定窗口如果在窗口开始就打满阈值,窗口后半部分进入的请求都会拒绝。

  2. 滑动窗口算法

    在固定窗口的基础上,窗口会随着时间向前推移,可以在时间内平滑控制流量,解决固定窗口出现的突发流量问题。

  3. 漏斗算法

    请求来了先进入漏斗,漏斗以恒定的速率放行请求。

  4. 令牌桶算法

    在令牌桶中,以恒定的速率放入令牌,令牌桶也有一定的容量,如果满了令牌就无法放进去了。拿到令牌的请求通过,并消耗令牌,如果令牌桶中令牌为空,则会丢弃该请求。

redis实现滑动窗口算法

当有请求来的时候记录时间戳,统计窗口内请求的数量时只需要统计redis中记录的数量。可以使用redis中的zset结构来存储。key可以设置为请求的资源名,同时根据限流的对象,往key中加入限流对象信息。比如根据ip限制访问某个资源的流量,可以使用方法名+ip作为key。score设置为时间戳。value则可以根据请求参数等信息生成MD5,或者直接生成UUID来存入,防止并发时多个请求存入的score和value一样导致只存入一个数据。

步骤如下:

  1. 定义时间窗口
  2. 请求到来,丢弃时间窗口之外的数据,ZREMRANGEBYSCORE KEYS[i], -inf, window_start
  3. 判断时间窗口内的请求个数是否达到阈值。ZCARD KEYS[i] 要小于阈值
  4. 如果小于则通过zadd加入,超过则返回不放行

lua脚本:

local window_start = tonumber(ARGV[1])- tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)
local current_requests = redis.call('ZCARD', KEYS[1])
if current_requests < tonumber(ARGV[3]) thenredis.call('ZADD', KEYS[1], tonumber(ARGV[1]), ARGV[4])return 1
elsereturn 0
end

java通过注解+切面实现限流

在java中,我们的需求是对资源可以进行多种规则的限流。注解可以定义不同类型的限流,如:全局限流,根据IP限流,根据用户限流。对每种类型的限流可以在一个注解中定义多个限流规则。

整体效果如下:

@RateLimiter(rules = {@RateLimitRule(time = 50,count = 100),@RateLimitRule(time = 20,count = 10)}, type = LimitType.IP)
@RateLimiter(rules = {@RateLimitRule(time = 60,count = 1000)}, type = LimitType.DEFAULT)
public void update(){}

定义注解

定义了三个注解:

  1. RateLimiter:限流注解
  2. RateLimitRule:限流规则
  3. RateLimiters:存放多个限流注解的容器,为了可以重复使用该注解

RateLimiter:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 支持重复注解
@Repeatable(value = RateLimiters.class)
public @interface RateLimiter {/*** 限流键前缀** @return*/String key() default "rate_limit:";/*** 限流规则** @return*/RateLimitRule[] rules() default {};/*** 限流类型** @return*/LimitType type() default LimitType.DEFAULT;
}

RateLimitRule:

public @interface RateLimitRule {/*** 时间窗口, 单位秒** @return*/int time() default 60;/*** 允许请求数** @return*/int count() default 100;
}

RateLimiters:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiters {RateLimiter[] value();
}

改造lua脚本

在实现切面之前,我们需要对lua脚本进行改造。我们的需求对资源可以进行多种规则的限流。根据限流类型和限流规则可以组合出不同的key,比如我们要对某个资源进行以下规则限流:全局限流(60s,1000次; 600s,5000次),根据ip限流(2s,5次)。

根据这些规则我们就需要使用3个zset分别来存放请求记录。并且当三个规则都没达到阈值时才放行请求,否则拒绝请求。

对lua脚本改造,支持多个key。

 
local flag = 1
for i = 1, #KEYS dolocal window_start = tonumber(ARGV[1])- tonumber(ARGV[(i-1)*3+2])redis.call('ZREMRANGEBYSCORE', KEYS[i], '-inf', window_start)local current_requests = redis.call('ZCARD', KEYS[i])if current_requests < tonumber(ARGV[(i-1)*3+3]) thenelseflag = 0end
end
if flag == 1 thenfor i = 1, #KEYS doredis.call('ZADD', KEYS[i], tonumber(ARGV[1]), ARGV[(i-1)*3+4])end
end
return flag

定义切面

定义一个切面实现限流逻辑:RateLimiterAspect

首先定义切点,由于我们可以重复使用注解,所以需要把RateLimiter和RateLimiters都定义为切点

@Pointcut("@annotation(com.imgyh.framework.annotation.RateLimiter)")
public void rateLimiter() {
}@Pointcut("@annotation(com.imgyh.framework.annotation.RateLimiters)")
public void rateLimiters() {
}

在前置通知中实现限流逻辑:

主要流程如下:

  1. 把所有的RateLimiter都拿到,解析出限流规则和限流类型
  2. 根据限流规则和限流类型,获取所有的key和参数,为调用lua脚本做准备
  3. 调用lua脚本,根据返回值判断是否放行请求
// 定义切点之前的操作
@Before("rateLimiter() || rateLimiters()")
public void doBefore(JoinPoint point) {try {// 从切点获取方法签名MethodSignature signature = (MethodSignature) point.getSignature();// 获取方法Method method = signature.getMethod();String name = point.getTarget().getClass().getName() + "." + signature.getName();// 获取日志注解RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);RateLimiters rateLimiters = method.getAnnotation(RateLimiters.class);List<RateLimiter> limiters = new ArrayList<>();if (ObjectUtils.isNotNull(rateLimiter)) {limiters.add(rateLimiter);}if (ObjectUtils.isNotNull(rateLimiters)) {limiters.addAll(Arrays.asList(rateLimiters.value()));}if (!allowRequest(limiters, name)) {throw new ServiceException("访问过于频繁,请稍候再试");}} catch (ServiceException e) {throw e;} catch (Exception e) {throw new RuntimeException("服务器限流异常,请稍候再试");}
}/*** 是否允许请求** @param rateLimiters 限流注解* @param name         方法全名* @return 是否放行*/
private boolean allowRequest(List<RateLimiter> rateLimiters, String name) {List<String> keys = getKeys(rateLimiters, name);Object[] args = getArgs(rateLimiters);Object res = redisTemplate.execute(limitScript, keys, args);return ObjectUtils.isNotNull(res) && (Long) res == 1L;
}/*** 获取限流的键** @param rateLimiters 限流注解* @param name         方法全名* @return*/
private List<String> getKeys(List<RateLimiter> rateLimiters, String name) {List<String> keys = new ArrayList<>();for (RateLimiter rateLimiter : rateLimiters) {String key = rateLimiter.key();RateLimitRule[] rules = rateLimiter.rules();LimitType type = rateLimiter.type();StringBuilder sb = new StringBuilder();sb.append(key).append(name);if (LimitType.IP == type) {String ipAddr = IpUtils.getIpAddr();sb.append("_").append(ipAddr);} else if (LimitType.USER == type) {Long userId = SecurityUtils.getUserId();sb.append("_").append(userId);}for (RateLimitRule rule : rules) {int time = rule.time() * 1000;int count = rule.count();StringBuilder builder = new StringBuilder(sb);builder.append("_").append(time).append("_").append(count);keys.add(builder.toString());}}return keys;
}/*** 获取需要的参数** @param rateLimiters 限流注解* @return*/
private Object[] getArgs(List<RateLimiter> rateLimiters) {List<Object> args = new ArrayList<>();args.add(System.currentTimeMillis());for (RateLimiter rateLimiter : rateLimiters) {RateLimitRule[] rules = rateLimiter.rules();for (RateLimitRule rule : rules) {int time = rule.time() * 1000;int count = rule.count();args.add(time);args.add(count);args.add(IdUtils.fastSimpleUUID());}}return args.toArray();
}

实例demo演示

demo源码仓库:github.com/imgyh/devel…

定义接口,并添加限流注解。

限制对某个用户只能1s中访问2次。对接口整体10s中访问50次,60秒访问100次。

当某个用户一秒钟请求超过两次时,抛出异常。

参考资源

  1. Hollis,《Java面试宝典》
  2. 一文搞懂高频面试题之限流算法,从算法原理到实现,再到对比分析

相关文章:

Redis实现滑动窗口限流

常见限流算法 固定窗口算法 在固定的时间窗口下进行计数&#xff0c;达到阈值就拒绝请求。固定窗口如果在窗口开始就打满阈值&#xff0c;窗口后半部分进入的请求都会拒绝。 滑动窗口算法 在固定窗口的基础上&#xff0c;窗口会随着时间向前推移&#xff0c;可以在时间内平滑控…...

SQL Server查询计划(Query Plan)——XML查询计划

​​​​​​6.4.3. XML查询计划 SQL Server中,除了通过GUI工具和相关命令获取图形及文本查询计划外,我们还可以通过相关命令获取XML格式的查询计划,这里惯称其为XML查询计划。 SQL Server 2005版本引入了XML查询计划的新特性,其充分吸收了图形及文本查询计划的优势所在,…...

【day02】每天三道 java后端面试题:Java、C++和Go的区别 | Redis的特点和应用场景 | 计算机网络七层模型

文章目录 1. Java、C和 Go 语言的区别&#xff0c;各自的优缺点&#xff1f;2. 什么是Redis&#xff1f;Redis 有哪些特点&#xff1f; Redis有哪些常见的应用场景&#xff1f;3. 简述计算机网络七层模型和各自的作用&#xff1f; 1. Java、C和 Go 语言的区别&#xff0c;各自的…...

【Flink状态管理(八)】Checkpoint:CheckpointBarrier对齐后Checkpoint的完成、通知与对学习状态管理源码的思考

文章目录 一. 调用StreamTask执行Checkpoint操作1. 执行Checkpoint总体代码流程1.1. StreamTask.checkpointState()1.2. executeCheckpointing1.3. 将算子中的状态快照操作封装在OperatorSnapshotFutures中1.4. 算子状态进行快照1.5. 状态数据快照持久化 二. CheckpointCoordin…...

防御保护第八、九、十、十一天笔记

一、内容安全 1、DFI和DPI技术 --- 深度检测技术 DPI是一种基于应用层的流量检测和控制技术&#xff0c;它会对流量进行拆包&#xff0c;分析包头和应用层的内容&#xff0c;从而识别应用程序和应用程序的内容。这种技术增加了对应用层的分析&#xff0c;识别各种应用&#xf…...

【TypeScript基础知识点】的讲解

TypeScript基础知识点 TypeScript基础知识点 TypeScript基础知识点 TypeScript 是一种由 Microsoft 开发和维护的开源编程语言&#xff0c;它是 JavaScript 的一个超集&#xff0c;添加了可选的静态类型和基于类的面向对象编程&#xff0c;以下是一些 TypeScript 的基础知识点…...

牛客周赛 Round 34 解题报告 | 珂学家 | 构造思维 + 置换环

前言 整体评价 好绝望的牛客周赛&#xff0c;彻底暴露了CF菜菜的本质&#xff0c;F题没思路&#xff0c;G题用置换环骗了50%, 这大概是唯一的亮点了。 A. 小红的字符串生成 思路: 枚举 a,b两字符在相等情况下比较特殊 a, b input().split() if a b:print (2)print (a)pri…...

LeetCode13 罗马数字转整数

题目 罗马数字包含以下七种字符: I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如&…...

【Hudi】Upsert原理

17张图带你彻底理解Hudi Upsert原理 1.开始提交&#xff1a;判断上次任务是否失败&#xff0c;如果失败会触发回滚操作。然后会根据当前时间生成一个事务开始的请求标识元数据。2.构造HoodieRecord Rdd对象&#xff1a;Hudi 会根据元数据信息构造HoodieRecord Rdd 对象&#xf…...

信息系统服务:演绎数字时代的征程

信息系统服务作为数字化时代的基石&#xff0c;已经在人类社会的各个领域发挥着重要作用。本文将从信息系统服务的起源、发展和演化过程&#xff0c;通过生动的例子和准确客观的历史事实&#xff0c;探讨信息系统服务对人类社会的影响与变革。 1. 起源&#xff1a;信息处理的初…...

rust连接postgresql数据库

引入crate&#xff1a; postgres "0.19.7" use postgres::{Client, NoTls, error::Error};fn main() -> Result<(), Error> {let mut client Client::connect("hostlocalhost port5432 dbnamexxxxdb userpostgres passwordxxxxxx", NoTls).un…...

[面试] 什么是死锁? 如何解决死锁?

什么是死锁 死锁&#xff0c;简单来说就是两个或者多个的线程在执行的过程中&#xff0c;争夺同一个共享资源造成的相互等待的现象。如果没有外部干预线程会一直阻塞下去. 导致死锁的原因 互斥条件&#xff0c;共享资源 X 和 Y 只能被一个线程占用; 请求和保持条件&#xf…...

网络原理 HTTP _ HTTPS

回顾 我们前面介绍了HTTP协议的请求和响应的基本结构 请求报文是由首行请求头空行正文来组成的 响应报文是由首行形影头空行响应正文组成的 我们也介绍了一定的请求头之中的键值对的属性 Host,Content-type,Content-length,User-agent,Referer,Cookie HTTP协议中的状态码 我们先…...

软件实际应用实例,茶楼收银软件管理系统操作流程,茶室计时计费会员管理系统软件试用版教程

软件实际应用实例&#xff0c;茶楼收银软件管理系统操作流程&#xff0c;茶室计时计费会员管理系统软件试用版教程 一、前言 以下软件以 佳易王茶社计时计费管理系统软件V17.9为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、计时计费&…...

网络安全“三保一评”深度解析

“没有网络安全就没有国家安全”。近几年&#xff0c;我国法律法规陆续发布实施&#xff0c;为承载我国国计民生的重要网络信息系统的安全提供了法律保障&#xff0c;正在实施的“3保1评”为我国重要网络信息系统的安全构筑了四道防线。 什么是“3保1评”&#xff1f; 等保、分…...

IDA使用-2023CICSN华中赛区pwn题逆向为例

文章目录 相关字节标识导入函数和导出函数找程序入口函数选项设置重命名CISCN2023华中赛区分区赛AWDIDA源码main 构造结构体sub_141B() 打开局部变量类型的视图增加变量类型重新定义变量类型再次设置变量类型并重新定义再次设置变量类型并重新定义再次设置变量类型并重新定义 设…...

安装虚拟机出现的一些问题

1、在重新打开软件之后出现闪退 解决&#xff1a;[WSL] 解决nsenter: cannot open /proc/320/ns/time: No such file or directory 问题 小白向-CSDN博客2、重新启动xrdp服务命令 解决&#xff1a; sudo systemctl restart xrdp3、将端口从3389改为3390&#xff0c;因为此前…...

Git+py+ipynb Usage

0.default config ssh-keygen -t rsa #之后一路回车,当前目录.ssh/下产生公私钥 cat ~/.ssh/id_rsa.pub #复制公钥到账号 git config --global user.email account_email git config --global user.name account_namebug of ipynb TqdmWarning: IProgress not found. Please …...

eBPF实践篇之环境搭建

文章目录 前言实验环境前置知识配置开发环境最后 前言 你好&#xff0c;我是醉墨居士&#xff0c;本次我们学习一下eBPF&#xff0c;我们基于libbpf-bootstrap来进行我们的eBPF程序开发&#x1f917; 实验环境 一台Debian12操作系统的计算机&#xff0c;我使用的是Debian12.…...

机器学习科普及学习路线

机器学习是一种让计算机系统通过从数据中学习来改进性能的方法。它的学习方法主要包括监督学习、无监督学习和强化学习。下面我将详细解释机器学习的概念、学习方法和学习路线。 1. 机器学习概念&#xff1a; 机器学习是一种人工智能的分支&#xff0c;旨在使计算机系统能够从…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

高防服务器能够抵御哪些网络攻击呢?

高防服务器作为一种有着高度防御能力的服务器&#xff0c;可以帮助网站应对分布式拒绝服务攻击&#xff0c;有效识别和清理一些恶意的网络流量&#xff0c;为用户提供安全且稳定的网络环境&#xff0c;那么&#xff0c;高防服务器一般都可以抵御哪些网络攻击呢&#xff1f;下面…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

MFC 抛体运动模拟:常见问题解决与界面美化

在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...