Redis-限流方案

server/2025/3/12 1:50:25/

在实际业务中,可能会遇到瞬时流量剧增的情况,大量的请求可能会导致服务器过载和宕机。为了保护系统自身和上下游服务,需要采用限流的方式,拒绝部分请求。
限流就是对请求的频率进行控制,迅速拒绝超过请求阈值的请求。
比如:Redis的QPS为10w,但是此时20w个请求同一时刻请求到Redis,造成Redis性能降低。
[图片]

1. 什么是限流

限流就是在高并发场景下,通过限制系统处理请求的速率,迅速拒绝超过设定上限的请求(最大的QPS),从而保证系统正常运行。
在限流技术中,主要是有两个注意点:当前系统请求的阈值和拒绝策略。
请求的阈值:阈值就是单位时间内允许请求的最大请求数。例如,将QPS(Queries Per Second)设置为1000,表明1s内最多接受1000个请求。通过设置适当的阈值,可以有效控制系统负载,避免系统请求过多而崩溃。(阈值是上线前通过压测或者评估得出的指标)

拒绝策略: 处理超过阈值的请求的方法,常见的拒绝策略包括直接拒绝和等待。

  • 直接拒绝策略:直接拒绝超过阈值的请求,直接返回给客户端。(提示:当前抢购人数太多)
  • 排队等待策略:将请求放入队列中,按照一定规则处理,避免瞬间拒绝大量请求。

2. 常见的限流算法

常见的限流算法分为:固定窗口(计数器限流)、滑动窗口、漏桶、令牌桶 四个方案

2.1. 固定窗口限流算法

在一段时间间隔内,处理请求的最大数量。

固定窗口其实就是时间窗口,原理是将时间划分成固定大小的窗口,并且在每个窗口内限制请求的数量和速率,如果某个窗口内的请求超过了阈值,就直接执行拒绝策略。

即:固定窗口限流算法是规定了单位时间处理的请求数量。

算法步骤:

  1. 将时间划分为固定大小的窗口,例如每秒一个。
  2. 在每一个时间窗口内,记录请求的数量。
  3. 当新的请求到达时,当前窗口的请求计数器的值加一。
  4. 如果窗口内请求计数器超过阈值,那么执行拒绝策略。
  5. 当窗口结束后,重置计数器。
    在这里插入图片描述

例如,每个时间窗口为1s,限流的阈值为3,窗口内超过阈值的请求都会触发拒绝策略。
优点:实现简单,易于理解。
缺点:

  • 请求不够均匀:在固定窗口算法中,请求在窗口内的分布可能会不均匀。假如限制某个接口每分钟只能访问30次,那么前30秒就会有30个请求到达的话,那么后30秒就无法处理请求,导致请求不均匀。
  • 无法保证限流速率,因而无法因对突增的流量。比如一个接口一分钟只能访问1000次,但是接口的QPS为500,前55秒这个接口没有收到请求,但是后一秒突然接收到1000个请求。然后,在当前场景下,这1000个请求在1s内是无法被处理的,系统可能就直接被大量的请求给击垮了。
  • 存在明显的临界问题(请求突刺问题):前一个窗口和后一个窗口之间的可能会存在大量的请求,无法应对无法流量。
    比如:一个窗口内请求的阈值为3,以下存在临界问题:
    在这里插入图片描述
public class FixedWindowLimiter {private static final String FORMAT_TIME = "yyyy-MM-dd HH:mm:ss";private final long windowSize; // 窗口大小,默认是毫秒private final int maxRequests; // 最大请求数private int currentRequests; //当前窗口内的请求数private LocalDateTime lastRestTime; //上次窗口重置时间,即窗口开始时间private final Lock resetMutex; //重置锁public FixedWindowLimiter(Duration windowSize, int maxRequests) {this.windowSize = windowSize.toMillis();this.maxRequests = maxRequests;this.currentRequests = 0;this.lastRestTime = LocalDateTime.now();this.resetMutex = new ReentrantLock();}public boolean allow() {resetMutex.lock();try {LocalDateTime now = LocalDateTime.now();if (Duration.between(lastRestTime, now).toMillis() >= windowSize) {this.currentRequests = 0;this.lastRestTime = now;}if (currentRequests >= maxRequests) {return false;}currentRequests++;return true;}finally {resetMutex.unlock();}}
}class FixedWindowLimiterApp {private static final String FORMAT_TIME = "yyyy-MM-dd HH:mm:ss";public static void main(String[] args) {System.out.println(System.currentTimeMillis() / 1000);FixedWindowLimiter limiter = new FixedWindowLimiter(Duration.ofSeconds(1), 3);DateTimeFormatter formatter = DateTimeFormatter.ofPattern(FORMAT_TIME);ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);for (int i = 0; i < 20; i++) {Runnable task = () -> {String now = LocalDateTime.now().format(formatter);if (limiter.allow()) {System.out.println(now + "请求通过");} else {System.out.println(now + "请求被限流");}};pool.schedule(task, i * 100, TimeUnit.MILLISECONDS);}pool.shutdown();try {if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {pool.shutdownNow();}} catch (InterruptedException e) {pool.shutdownNow();}}
}

2.2. 滑动窗口限流算法

滑动窗口限流算法是固定窗口限流算法的升级版本,限流的粒度更小。
滑动窗口和固定窗口相比:滑动窗口是将时间按照一定的比例分片。
假如接口限流每分钟处理60个请求,可以将1分钟分成60个小窗口,然后每隔一秒移动一次,每个窗口一秒只能处理不大于60(请求数) / 60(窗口数),如果当前窗口的请求计数综合超过了限制的数量的话,就触发拒绝策略。
[图片]

这样的话,可以处理[0.5秒 ~ 1.5秒]内的临界问题,红色部分的请求将会触发拒绝策略。
优点:

  • 相比于固定窗口算法,滑动窗口计数器算法可以应对突发流量,解决一些临界问题。
  • 限流粒度更小,可以提供更精确的限流控制。
    缺点:
  • 和固定窗口一样,存在请求不够均匀的情况。

2.3. 漏桶限流算法

漏桶限流的核心思想就是将请求存储在一个漏桶中,无论我们以任意速率存入请求,漏桶始终以固定的速率流出。所以漏桶算法可以将突发流量均匀地处理,确保系统在稳定的负载下运行。
[图片]

优点:

  1. 实现简单,易于理解。
  2. 可以控制限流速率,避免网络拥塞和系统过载。
    缺点:
  3. 在突发情况请求过多时,会丢弃过多的请求。

2.4. 令牌桶限流算法

令牌桶算法也比较简单,和漏桶算法一样,主角还是桶。不过现在桶里面装的是令牌,请求在被处理之前需要拿到一个令牌,请求处理完毕之后直接丢弃令牌即可。我们可以根据限流大小,按照一定的速率往桶里添加令牌,如果桶装满了,就不能继续往桶里添加令牌了。


优点:

  1. 可以应对突发流量:由于令牌可以在桶中堆积,当流量突然增大时,如果桶中有足够的令牌,系统可以快速响应这种突发流量,避免请求被立即拒绝。这使得令牌桶算法特别适合处理具有突发性流量的应用场景。
  2. 灵活性强:通过适当调整流派的生成速率和桶的容量,可以灵活地控制流量。
    算法设计:
public class TokenBucketLimiter {private final int rate; // 令牌产生的速度,即每秒生成多少个令牌private final int capacity; // 令牌桶容量,最多可存储令牌数private int currentTokenNum; // 当前令牌数private LocalDateTime lastTime; // 上次请求时间private final Lock lock; // 请求锁public TokenBucketLimiter(int rate, int capacity) {this.rate = rate;this.capacity = capacity;this.currentTokenNum = 0;this.lastTime = LocalDateTime.now();this.lock = new ReentrantLock();}public int getCurrentTokenNum() {return currentTokenNum;}public int getRate() {return rate;}public int getCapacity() {return capacity;}public boolean allow() {lock.lock();try {// 计算当前时间和上一次更新时间的时间间隔long timeDiff = Duration.between(lastTime, LocalDateTime.now()).toMillis();// 计算时间间隔内生成的令牌数量int tokenCount = (int) (timeDiff / 1000.0 * rate);if (tokenCount > 0) {currentTokenNum += tokenCount;lastTime = LocalDateTime.now();}// 令牌的数量不能超过桶的大小if (currentTokenNum > capacity) {currentTokenNum = capacity;}// 获取令牌if (currentTokenNum > 0) {currentTokenNum--;return true;}return false;} finally {lock.unlock();}}}class TokenBucketLimiterApp {public static void mockRequest(int n, Duration d, TokenBucketLimiter limiter) {ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);for (int i = 0; i < n; i++) {int requestId = i + 1;executor.schedule(() -> {if (limiter.allow()) {System.out.printf("第%d个请求通过\n", requestId);} else {System.out.printf("第%d个请求被限流\n", requestId);}}, i * d.toMillis(), TimeUnit.MILLISECONDS);}executor.shutdown();try {if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {executor.shutdownNow();}} catch (InterruptedException e) {executor.shutdownNow();}}public static void main(String[] args) {System.out.println("=================令牌桶算法=================");// 创建一个令牌桶,令牌的生成速度为每秒4个,桶容量为5个请求TokenBucketLimiter limiter = new TokenBucketLimiter(4, 5);// 发送10个请求,每50ms发送一个mockRequest(10, Duration.ofMillis(50), limiter);System.out.println("------------------------------------------");}
}

3. 针对什么进行限流?

实际项目中,针对的先流对象是什么?也就是说针对什么来进行限流?常见的限流对象如下:

  1. 针对IP限流:比较常见,比如通过X-forwarded-For或TCP options字段中真实源IP信息。
  2. 业务ID:挑选唯一的业务ID实现限流。比如,根据用户的ID进行限流。(写leetcode的时候多次提交,提醒提交太快)。
  3. 个性化:根据用户的属性或者行为。进行不同的限流策略。例如,VIP用户不限流,普通用户限流。

4. 单机限流

单机限流是指在单台服务器上,通过限制其在单位时间内处理的请求数量来防止过载。
单机限流可以使用Google Guava自带的限流工具类 RateLimiter。RateLimiter是基于令牌桶算法实现的,可以应对突发流量。

5. 分布式限流

单机限流只能保证一个节点限流,但是无法保证分布式限流。
分布式限流和分布式锁的思想是一样的,就是将原本设计实现在本地服务的限流操作抽离出来,做成一个中心化的限流器,实现全局共享。


分布式限流常见方案:可以借助Sentinel或者使用Redis来自己实现对应的限流逻辑。
基于Redis的实现步骤如下:(Redis + Lua)

  1. 选取一个Redis中心化组件
  2. 设定限流规则,比如每秒允许的最大请求数(QPS),并且将该值存储在Redis中。
  3. 每当有请求到达时,服务器首先会向Redis请求令牌。
  4. 如果获得令牌,请求可以继续处理;如果未获得令牌,则表示请求被限流,执行拒绝策略。
local key = KEYS[1] -- hashmap的key
local rate = tonumber(ARGV[1]) -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶的容量
local now = tonumber(ARGV[3]) -- 当前时间(毫秒)
local requested = tonumber(ARGV[4]) -- 请求的令牌数-- 获取上次更新时间和当前令牌数
local last_time = redis.call('HGET', key, 'last_time')
local current_tokens = redis.call('HGET', key, 'tokens')-- 如果是首次访问,那么就将last_time初始化为当前时间,当前令牌数为桶容量
if last_time == false thenlast_time = nowcurrent_tokens = capacity
else-- 不是第一次访问,那么从redis获取上次访问时间和当前令牌数last_time = tonumber(last_time)current_tokens = tonumber(current_tokens)
end-- 计算时间差
local time_diff = now - last_time
-- 计算新的令牌数
local new_tokens = math.min(capacity, current_tokens + (time_diff * rate / 1000))local allowed = new_tokens >= requestedif allowed thennew_tokens = new_tokens - requested
endredis.call('HSET', key, 'last_time', now)
redis.call('HSET', key, 'tokens', new_tokens)return allowed

http://www.ppmy.cn/server/173967.html

相关文章

RMAN备份bug-审计日志暴涨(select action from gv$session)

问题概述 /oracle 文件系统使用率过大&#xff0c;经过检查是审计日志过大,/oracle 目录 197G 审计日志占用70G&#xff0c;每6个小时产生大量审计日志&#xff0c;日志内容全是select action from gv$session &#xff0c;猜测可能跟备份有关&#xff0c; $>df -h /oracle…

Git学习笔记(二)

Git学习笔记&#xff08;二&#xff09; 下载VSCode创建本地仓库推送远程仓库界面功能 使用 VSCode 进行Git仓库的项目管理 这篇文章是我学完使用 命令行终端 管理Git仓库额外学习的 文章主要用于巩固和方便后续复习 下载VSCode 可以看我这篇文章下载VSCode 创建本地仓库 …

Android15使用FFmpeg解码并播放MP4视频完整示例

效果: 1.编译FFmpeg库: 下载FFmpeg-kit的源码并编译生成安装平台库 2.复制生成的FFmpeg库so文件与包含目录到自己的Android下 如果没有prebuiltLibs目录,创建一个,然后复制 包含目录只复制arm64-v8a下

百货店的诞生与现代商业革命:结合开源AI智能客服、AI智能名片与S2B2C商城小程序的新视角

摘要&#xff1a;本文深入探讨了百货店作为现代商业革命的标志性事件&#xff0c;其出现对销售方式、经营方式、组织管理三个方面的根本性变革。同时&#xff0c;本文也展望了在数字化时代背景下&#xff0c;开源AI智能客服、AI智能名片以及S2B2C商城小程序等新兴技术如何为传统…

第9章 管理日志(网络安全防御实战--蓝军武器库)

网络安全话题下的优秀答主 10 人赞同了该文章 网络安全防御实战--蓝军武器库是2020年出版的&#xff0c;已经过去3年时间了&#xff0c;最近利用闲暇时间&#xff0c;抓紧吸收&#xff0c;总的来说&#xff0c;第9章开始学习windows事件日志和syslog管理日志&#xff0c;在当…

Django 中,Form 和 ModelForm的用法和区别

在 Django 中,Form 和 ModelForm 是用于处理表单数据的两种主要方式。它们的主要区别在于是否与模型(Model)直接关联。以下是它们的用法、区别以及高级用法的详细说明: 一、Form 的使用 1. 基本用法 Form 是一个独立的表单类,不与任何模型直接关联。适用于需要手动定义字…

力扣刷题11. 盛最多水的容器

11. 盛最多水的容器 - 力扣&#xff08;LeetCode&#xff09; 暴力解法就是 双层循环&#xff0c;遍历寻找最大的值&#xff0c;但是会超时 这一道题可以用双指针的解法&#xff0c;左指针在最左边&#xff0c;有指针指向最右边&#xff0c;每次判断左右指针的值大小&#xff…

【音视频】ffplay播放控制

一、ffplay播放控制 1.1、ffplay打开视频 比如我当前目录下现在有一个1.mp4的视频&#xff0c;可以使用下面的命令用ffplay打开并播放它 ffplay 1.mp4输入后回车即可打开相应的视频 1.2 ffplay播放控制 使用q、ESC退出播放按f、双击切换全屏状态按m切换为静音按9减少音量&a…