基于 Redis 实现分布式限流
基于 Redis 实现分布式限流
- 一、 简介
- 二、分布式限流
- 1 数据结构
- 1.1 Redis List
- 1.2 Redis Set
- 1.3 Redis Sorted Set
- 2 实现分布式限流
- 3 实现原理分析
- 三、分布式限流算法
- 1. 计数器算法
- 2. 漏斗算法
- 3. 令牌桶算法
- 四、分布式限流实战
- 1. 单机限流实现
- 2. 基于Redis Clusters的分布式限流实现
- 五、基于Redis分布式限流的优化
- 1. 缓存击穿
- 1.1 问题描述
- 1.2 解决方案
- 1.2.1 使用互斥锁
- 1.2.2 使用预热机制
- 2. 热点key问题的解决方案
- 2.1 问题描述
- 2.2 解决方案
- 2.2.1 分布式锁
- 2.2.2 分布式缓存
- 3. 并发竞争优化
- 3.1 问题描述
- 3.2 解决方案
- 3.2.1 使用限流器
- 3.2.2 使用异步线程池
一、 简介
分布式限流是指通过将限流策略嵌入到分布式系统中,以控制流量或保护服务,保证系统在高并发访问情况下不被过载。
分布式限流可以防止系统因大量请求同时到达导致压力过大而崩溃,从而提高系统的稳定性和可靠性。同时,它可以使得业务资源能够更好地分配,提高系统的效率。
二、分布式限流
1 数据结构
1.1 Redis List
Redis List 是一个可以输入相同元素和非唯一元素的集合且支持在两端进行快速(O(1))插入和删除元素。
1.2 Redis Set
Redis Set 是一个无序,但不允许重复元素的集合。您可以使用这些命令对Redis集进行常规操作: SADD,SREM,SISMEMBER,SMEMBERS等。
1.3 Redis Sorted Set
Redis Sorted Set 是一个有序的、不重复的元素集合。每个元素都关联着一个浮点数值(称为 Score)。通过 Score 可以从小到大排序得到一个有序集合。
2 实现分布式限流
在Redis中可以使用令牌桶算法来实现分布式限速。具体方法为:
Step 1:创建一个列表作为Redis令牌桶
String key = "rate_limit:" + userId;
// 模拟用户请求访问
List<String> tokens = redis.lrange(key, 0, -1);
Step 2:设定令牌桶的基准参数
int maxTokens = 50;
long timeInterval = 1 * 1000;
long now = System.currentTimeMillis();
Step 3:计算Redis中令牌数量
long expiredTokens = tokens.stream().filter(t -> Long.parseLong(t) < now - timeInterval).count();
tokens = tokens.subList((int) expiredTokens, tokens.size());
long remainTokens = maxTokens - tokens.size();
Step 4:基于令牌数量,判断是否超过限制
if (remainTokens < 1) {throw new RateLimitException("请求太频繁,请稍后再试!");
}
Step 5:如果没有超出限制,则更新Redis中令牌数并设置过期时间
Long expiresIn = now + timeInterval;
redis.multi();
redis.rpush(key, String.valueOf(expiresIn));
redis.pexpire(key, timeInterval);
redis.exec();
3 实现原理分析
以上代码所示首先需要建立Redis List用于存储Token,其次需要设定令牌桶的基准参数(比如最大Token数量和Token过期间隔等)。在用户访问请求时,需要计算Redis中的令牌数量,根据规则对访问量进行限制。如果没有超过限制,则需要更新Redis List中令牌数并设置过期时间;如果超过了限制,则需要返回错误信息并拒绝服务。
整个过程中,需要注意并发访问情况下的线程安全问题,并确保流量控制配置的公共协商,如最大QPS(Queries Per Second),哪些接口需限制流量等。
三、分布式限流算法
在实际的系统设计中,为了防止某一时刻出现大量请求导致系统崩溃,我们通常会采用限流策略来控制流量,而Redis作为分布式NoSQL数据库,在限流中也有着广泛的应用。下面介绍一些Redis分布式限流的经典算法。
1. 计数器算法
计数器算法比较简单,直接利用Redis存储每个IP或者用户的请求次数,当请求次数超过预设阈值时拒绝服务。代码如下:
public boolean isAllowed(String key, int limit, int timeout) {Jedis jedis = getJedis();long count = jedis.incr(key);if (count == 1) {jedis.expire(key, timeout);}boolean allowed = count <= limit;if (!allowed) {jedis.del(key);}jedis.close();return allowed;
}
key:需要限流的用户标识,可根据IP、UserID等进行定义limit:阈值,即允许的最大请求数timeout:过期时间,对于计数器算法,一定要设置过期时间,否则缓存中的请求次数会一直不断累加
2. 漏斗算法
漏斗算法的核心思想是将请求按照恒定的速率转换为水流,有效控制请求超出服务处理能力的情况。漏斗算法实现代码如下:
public boolean isAllowed(String key, int capacity, double leakRate, int reqCount) {Jedis jedis = getJedis();long nowTime = System.currentTimeMillis();String luaScript ="local currentCapacity = tonumber(redis.call('hget', KEYS[1], 'leftCapacity'))\n"+ "if currentCapacity == nil then\n"+ " redis.call('hset', KEYS[1], 'lastTime', ARGV[2])\n"+ " redis.call('hset', KEYS[1], 'leftCapacity', ARGV[1] - 1)\n"+ " return 1\n"+ "end\n"+ "local changeTime = tonumber(redis.call('hget', KEYS[1], 'lastTime'))\n"+ "local delayMillSeconds = nowTime - changeTime\n"+ "local currentDelayCount = tonumber(delayMillSeconds*ARGV[3])\n"+ "local currentCapacity = math.min(currentDelayCount+currentCapacity, ARGV[1])\n"+ "if currentCapacity >= ARGV[4] then\n"+ " return 0\n"+ "else\n"+ " redis.call('hset', KEYS[1], 'leftCapacity', currentCapacity-1)\n"+ " redis.call('hset', KEYS[1], 'lastTime', nowTime)\n"+ " return 1\n"+ "end";Object result = jedis.eval(luaScript,Collections.singletonList(key),Arrays.asList(String.valueOf(capacity), String.valueOf(nowTime), String.valueOf(leakRate), String.valueOf(reqCount)));boolean allowed = (result instanceof Long ? (Long) result : 0L) == 1L;jedis.close();return allowed;
}
key:需要进行限流的用户标识capacity:漏斗容量,即最大允许请求数量leakRate:漏嘴流水速率,保证有序的请求到达reqCount:预计请求量,用于计算漏斗每次流出的数量
3. 令牌桶算法
令牌桶算法的特点是以一个固定的速率不断产生令牌,并将令牌放入到桶中,访问时若桶为空,则表示请求数超限。令牌桶算法实现代码如下:
public boolean isAllowed(String key, int capacity, double rate, int reqCount) {long nowTime = System.currentTimeMillis();Jedis jedis = getJedis();String luaScript ="local currentLimit = tonumber(redis.call('get', KEYS[1]) or '0')\n"+ "if currentLimit + ARGV[1] > tonumber(KEYS[2]) then\n"+ " return false\n"+ "else\n"+ " redis.call('incrby', KEYS[1], ARGV[1])\n"+ " redis.call('expire', KEYS[1], ARGV[2])\n"+ " return true\n"+ "end";Object result = jedis.eval(luaScript, 2, key, String.valueOf(capacity), String.valueOf(reqCount), String.valueOf(rate * (nowTime / 1000)));boolean allowed = (result instanceof Boolean ? (Boolean) result : false);jedis.close();return allowed;
}
key:需要进行限流的用户标识capacity:桶容量rate:令牌发放速率reqCount:请求数量
四、分布式限流实战
1. 单机限流实现
假设我们有一个需求,需要限制每个IP一分钟内最多只能发送100个请求。可以通过Redis的INCR、EXPIRE等API操作来简单实现单机限流。
public boolean isAllowed(String ip, int limit, int interval) {Jedis jedis = getJedis();String key = "ip:" + ip;long count = jedis.incr(key);if (count == 1) {jedis.expire(key, interval);}boolean allowed = count <= limit;if (!allowed) {jedis.del(key);}jedis.close();return allowed;
}
2. 基于Redis Clusters的分布式限流实现
当业务规模扩大时,单机的限流已经无法满足需求,这时候需要考虑使用Redis Clusters实现分布式限流。Clusers扩展了原先Redis的功能,不仅支持横向扩展,而且提高了整个集群的可用性。限流算法同上,只是需要使用把数据分配到Cluser内不同的节点上。
public boolean isAllowed(String ip, int limit, int interval) {JedisCluster jedis = getJedisCluster();String key = "ip:" + ip;long count = jedis.incr(key);if (count == 1) {jedis.expire(key, interval);}boolean allowed = count <= limit;if (!allowed) {jedis.del(key);}return allowed;
}
五、基于Redis分布式限流的优化
1. 缓存击穿
1.1 问题描述
在高并发场景下,如果存在大量的缓存未命中请求,将会导致访问底层数据存储系统,这种情况被称为缓存击穿。
1.2 解决方案
1.2.1 使用互斥锁
/*** 获取缓存值方法* @param key 缓存键值* @return 缓存值*/
public String getCacheValue(String key) {String value = cache.get(key);if (value == null) { //缓存未命中//使用互斥锁Lock lock = redisson.getLock(key);if (lock.tryLock()) { //尝试获取锁try {value = cache.get(key); //再次尝试获取缓存if (value == null) { //如果仍然未命中,从数据库中获取value = db.get(key);cache.put(key, value); //将查询结果放入缓存}} finally {lock.unlock(); //释放锁}} else {Thread.sleep(100); //自旋一段时间后重试return getCacheValue(key);}}return value;
}
1.2.2 使用预热机制
预热机制是指在系统启动的时候,提前加载热点数据到缓存中,以减少缓存未命中请求。预热的方式可以使用定时任务或者其他方式,在系统低峰期进行加载。
2. 热点key问题的解决方案
2.1 问题描述
在高并发场景下,如果某个key的请求量过大,将会导致这个key成为热点key,从而导致缓存雪崩问题。
2.2 解决方案
2.2.1 分布式锁
/*** 获取缓存值方法* @param key 缓存键值* @return 缓存值*/
public String getCacheValue(String key) {String value = cache.get(key);if (value == null) { //缓存未命中//使用分布式锁RLock lock = redisson.getFairLock(key);if (lock.tryLock()) { //尝试获取锁try {value = cache.get(key); //再次尝试获取缓存if (value == null) { //如果仍然未命中,从数据库中获取value = db.get(key);cache.put(key, value); //将查询结果放入缓存}} finally {lock.unlock(); //释放锁}} else {Thread.sleep(100); //自旋一段时间后重试return getCacheValue(key);}}return value;
}
2.2.2 分布式缓存
使用分布式缓存将数据均匀地分散到多个节点上,从而避免单点瓶颈问题。
3. 并发竞争优化
3.1 问题描述
在高并发场景下,对于某些资源的并发访问将会导致性能瓶颈,需要进行并发竞争优化。
3.2 解决方案
3.2.1 使用限流器
限流器类似于信号灯,用于控制并发请求的数量,在高峰期可以采用漏桶算法或令牌桶算法进行限流。
3.2.2 使用异步线程池
对于一些耗时的操作,可以使用异步线程池进行处理,从而避免阻塞主线程,提升系统的并发能力。
相关文章:
基于 Redis 实现分布式限流
基于 Redis 实现分布式限流 一、 简介二、分布式限流1 数据结构1.1 Redis List1.2 Redis Set1.3 Redis Sorted Set 2 实现分布式限流3 实现原理分析 三、分布式限流算法1. 计数器算法2. 漏斗算法3. 令牌桶算法 四、分布式限流实战1. 单机限流实现2. 基于Redis Clusters的分布式…...
前端下载文件方式(Blob)
以下以下载图标svg文件为例,实现点击按钮下载文件,其中icon结构如下: const DownloadSvg (props) > {function download(downfile) {const tmpLink document.createElement("a");const objectUrl URL.createObjectURL(downfi…...
【STM32】FreeRTOS软件定时器学习
软件定时器 FreeRTOS提供了现成的软件定时器功能,可以一定程度上替代硬件定时器,但精度不高。 实验:创建一个任务,两个定时器,按键开启定时器,一个500ms打印一次,一个1000ms打印一次。 实现&…...
【LeetCode】复写零
复写零 题目描述算法描述编程代码 链接: 复写零 题目描述 算法描述 编程代码 class Solution { public:void duplicateZeros(vector<int>& arr) {int n arr.size();int dest -1,cur 0;while(cur < n){if(arr[cur]){dest;}else{dest2;}cur;if(dest > n-1){…...
使用docker-maven-plugin插件构建镜像并推送至私服Harbor
前言 如下所示,建议使用 Dockerfile Maven 插件,但该插件也停止维护更新了。因此先暂时使用docker-maven-plugin插件。 一、开启Docker服务器的远程访问 1.1 开启2375远程访问 默认的dokcer是不支持远程访问的,需要加点配置,开…...
YOLO目标检测——动漫头像数据集下载分享
动漫头像数据集是用于研究和分析动漫头像相关问题的数据集,它包含了大量的动漫风格的头像图像。动漫头像是指以动漫风格绘制的虚构人物的头像图像,常见于动画、漫画、游戏等媒体。 数据集点击下载:YOLO动漫头像数据集50800图片.rar...
学习Vue:Vue3 VS Vue2
Vue 3作为Vue.js的最新版本,带来了一系列令人激动的新特性和改进,让开发者们在构建现代Web应用时体验更加顺畅和高效。本文将全面介绍Vue 3相对于Vue 2的改进,重点解释Composition API的使用,以及新引入的Teleport和Suspense等特性…...
1.2亿成都市城市安全风险综合监测预警平台建设项目
导读:原文《1.2亿!成都市城市安全风险综合监测预警平台建设项目WORD》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。 部分页面: …...
《树莓派4B家庭服务器搭建指南》第二十期:在树莓派运行rsnapshot, 实现对服务器数据低成本增量本地备份
title: 020《树莓派4B家庭服务器搭建指南》第二十期:在树莓派运行rsnapshot, 实现对服务器数据低成本增量本地备份 我的天翼云服务器有/opt 和 /usr/share/nginx两个目录, 用来存储网站的内容, 数据无价, 为了避免珍贵的数据丢失,我决定使用树莓派运行 …...
大数据 算法
什么是大数据 大数据是指数据量巨大、类型繁多、处理速度快的数据集合。这些数据集合通常包括结构化数据(如数据库中的表格数据)、半结构化数据(如XML文件)和非结构化数据(如文本、音频和视频文件)。大数据…...
html | 基于iframe的简易富文本编辑器
效果图 支持: 选中后 ctrlI 斜体 代码 思路就是在iframe种嵌套html和css。 <pre> - 支持: 选中后 ctrlI 斜体 - todo: 鼠标实现单击斜体 </pre> <iframe name"richedit" style"height:30%; width:100%;"></iframe><script…...
HJ108 求最小公倍数
描述 正整数A和正整数B 的最小公倍数是指 能被A和B整除的最小的正整数值,设计一个算法,求输入A和B的最小公倍数。 数据范围:1≤a,b≤100000 1≤a,b≤100000 输入描述: 输入两个正整数A和B。 输出描述: 输出A和B…...
JVM - 垃圾收集器
目录 垃圾收集器 串行垃圾收集器 并行垃圾收集器 什么是 吞吐量优先 什么是 响应时间优先 ? CMS(并发)垃圾收集器 G1 垃圾收集器 垃圾收集器 垃圾收集器大概可以分为: 串行垃圾收集器并行垃圾收集器CMS(并发&a…...
华为数通方向HCIP-DataCom H12-821题库(单选题:21-40)
第21题 在广播类型网络中,DIS默认发送Hello时间间隔为多少? A、10s B、3.3s C、5S D、40s 答案:B 解析: 在广播环境中,DIS 发送 hello 报文的周期更加的短,是普通ISIS路由器的1/3,普通ISIS路由器发送hello的时间为10s,所以DIS发送hello的周期是3.3s …...
Springboot+mybaits-plus+h2集成产生的一些问题(not found tables)
一、问题描述 org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "EP_MAPPING" not found (this database is empty);大概就是说在引入mybatis-plus的依赖后,找不到数据库找不到表的问题。 排查方向:在引入mybatish2时,是可以正常…...
【C#学习笔记】C#特性的继承,封装,多态
文章目录 封装访问修饰符静态类和静态方法静态构造函数 继承继承原则sealed修饰符里氏替换原则继承中的构造函数 多态接口接口的实例化 抽象类和抽象方法抽象类和接口的异同 虚方法同名方法new覆盖的父类方法继承的同名方法 运行时的多态性编译时的多态性 照理继承封装多态应该…...
常用的电参数
电参数根据电流的特点可以分为直流电参数和交流电参数,在电参数中有些是可以通过电参数表测得,有些参数则为通过测得的参数计算而来。 一、电参数 1.1 直接可测电参数 ——瞬时电压值 ——瞬时电流值 n——采样点数 f——频率 time——时间 其中&…...
Rabbitmq的应用场景
Rabbitmq的应用场景 一、异步处理 场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式 串行方式: 将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有…...
【CSS动画08--流光按钮】
CSS动画08--流光按钮 介绍HTMLCSS 介绍 流光button HTML <!DOCTYPE html> <html><head><meta http-equiv"content-type" content"text/html; charsetutf-8"><meta name"viewport" content"widthdevice-width,i…...
计算机视觉:比SAM快50倍的分割一切视觉模型FastSAM
目录 引言 1 FastSAM介绍 1.1 FastSAM诞生 1.2 模型算法 1.3 实验结果 2 FastSAM运行环境构建 2.1 conda环境构建 2.2 运行环境安装 2.3 模型下载 3 FastSAM运行 3.1 命令行运行 3.1.1 Everything mode 3.1.2 Text prompt 3.1.3 Box prompt (xywh) 3.1.4 Points p…...
在rocky linux 9.5上在线安装 docker
前面是指南,后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...
Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
拟合问题处理
在机器学习中,核心任务通常围绕模型训练和性能提升展开,但你提到的 “优化训练数据解决过拟合” 和 “提升泛化性能解决欠拟合” 需要结合更准确的概念进行梳理。以下是对机器学习核心任务的系统复习和修正: 一、机器学习的核心任务框架 机…...
