主流的四种限流策略,我都可以通过redis实现
mhr18 2024-11-11 12:00 14 浏览 0 评论
引言
- 在web开发中功能是基石,除了功能以外运维和防护就是重头戏了。因为在网站运行期间可能会因为突然的访问量导致业务异常、也有可能遭受别人恶意攻击
- 所以我们的接口需要对流量进行限制。俗称的QPS也是对流量的一种描述
- 针对限流现在大多应该是令牌桶算法,因为它能保证更多的吞吐量。除了令牌桶算法还有他的前身漏桶算法和简单的计数算法
- 下面我们来看看这四种算法
固定时间窗口算法
- 固定时间窗口算法也可以叫做简单计数算法。网上有很多都将计数算法单独抽离出来。但是笔者认为计数算法是一种思想,而固定时间窗口算法是他的一种实现
- 包括下面滑动时间窗口算法也是计数算法的一种实现。因为计数如果不和时间进行绑定的话那么失去了限流的本质了。就变成了拒绝了
优点
- 在固定的时间内出现流量溢出可以立即做出限流。每个时间窗口不会相互影响
- 在时间单元内保障系统的稳定。保障的时间单元内系统的吞吐量上限
缺点
- 正如图示一样,他的最大问题就是临界状态。在临界状态最坏情况会受到两倍流量请求
- 除了临界的情况,还有一种是在一个单元时间窗内前期如果很快地消耗完请求阈值。那么剩下的时间将会无法请求。这样就会因为一瞬间的流量导致一段时间内系统不可用。这在互联网高可用的系统中是不能接受的。
实现
- 好了,关于原理介绍及优缺点我们已经了解了。下面我们动手实现它
- 首先我们在实现这种计数时,采用redis是非常好的选择。这里我们通过redis实现
controller
@RequestMapping(value = "/start",method = RequestMethod.GET)
public Map<String,Object> start(@RequestParam Map<String, Object> paramMap) {
return testService.startQps(paramMap);
}
service
@Override
public Map<String, Object> startQps(Map<String, Object> paramMap) {
//根据前端传递的qps上线
Integer times = 100;
if (paramMap.containsKey("times")) {
times = Integer.valueOf(paramMap.get("times").toString());
}
String redisKey = "redisQps";
RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory());
int no = redisAtomicInteger.getAndIncrement();
//设置时间固定时间窗口长度 1S
if (no == 0) {
redisAtomicInteger.expire(1, TimeUnit.SECONDS);
}
//判断是否超限 time=2 表示qps=3
if (no > times) {
throw new RuntimeException("qps refuse request");
}
//返回成功告知
Map<String, Object> map = new HashMap<>();
map.put("success", "success");
return map;
}
结果测试
- 我们设置的qps=3 , 我们可以看到五个并发进来后前三个正常访问,后面两个就失败了。稍等一段时间我们在并发访问,前三个又可以正常访问。说明到了下一个时间窗口
滑动时间窗口算法
- 针对固定时间窗口的缺点--临界值出现双倍流量问题。 我们的滑动时间窗口就产生了。
- 其实很好理解,就是针对固定时间窗口,将时间窗口统计从原来的固定间隔变成更加细度化的单元了。
- 在上面我们固定时间窗口演示中我们设置的时间单元是1S 。 针对1S我们将1S拆成时间戳。
- 固定时间窗口是统计单元随着时间的推移不断向后进行。而滑动时间窗口是我们认为的想象出一个时间单元按照相对论的思想将时间固定,我们的抽象时间单元自己移动。抽象的时间单元比实际的时间单元更小。
- 读者可以看下下面的动图,就可以理解了。
优点
- 实质上就是固定时间窗口算法的改进。所以固定时间窗口的缺点就是他的优点。
- 内部抽象一个滑动的时间窗,将时间更加小化。存在边界的问题更加小。客户感知更弱了。
缺点
- 不管是固定时间窗口算法还是滑动时间窗口算法,他们都是基于计数器算法进行优化,但是他们对待限流的策略太粗暴了。
- 为什么说粗暴呢,未限流他们正常放行。一旦达到限流后就会直接拒绝。这样我们会损失一部分请求。这对于一个产品来说不太友好
实现
- 滑动时间窗口是将时间更加细化,上面我们是通过redis#setnx实现的。这里我们就无法通过他统一记录了。我们应该加上更小的时间单元存储到一个集合汇总。然后根据集合的总量计算限流。redis的zsett数据结构就和符合我们的需求。
- 为什么选择zset呢,因为redis的zset中除了值以外还有一个权重。会根据这个权重进行排序。如果我们将我们的时间单元及时间戳作为我们的权重,那么我们获取统计的时候只需要按照一个时间戳范围就可以了。
- 因为zset内元素是唯一的,所以我们的值采用uuid或者雪花算法一类的id生成器
controller
@RequestMapping(value = "/startList",method = RequestMethod.GET)
public Map<String,Object> startList(@RequestParam Map<String, Object> paramMap) {
return testService.startList(paramMap);
}
service
String redisKey = "qpsZset";
Integer times = 100;
if (paramMap.containsKey("times")) {
times = Integer.valueOf(paramMap.get("times").toString());
}
long currentTimeMillis = System.currentTimeMillis();
long interMills = inter * 1000L;
Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis);
if (count > times) {
throw new RuntimeException("qps refuse request");
}
redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTimeMillis);
Map<String, Object> map = new HashMap<>();
map.put("success", "success");
return map;
结果测试
- 和固定时间窗口采用相同的并发。为什么上面也会出现临界状况呢。因为在代码里时间单元间隔比固定时间间隔采用还要大 。 上面演示固定时间窗口时间单元是1S出现了最坏情况。而滑动时间窗口设计上就应该间隔更短。而我设置成10S 也没有出现坏的情况
- 这里就说明滑动比固定的优处了。如果我们调更小应该更加不会出现临界问题,不过说到底他还是避免不了临界出现的问题
漏桶算法
- 滑动时间窗口虽然可以极大程度地规避临界值问题,但是始终还是避免不了
- 另外时间算法还有个致命的问题,他无法面对突如其来的大量流量,因为他在达到限流后直接就拒绝了其他额外流量
- 针对这个问题我们继续优化我们的限流算法。 漏桶算法应运而生
优点
- 面对限流更加的柔性,不再粗暴的拒绝。
- 增加了接口的接收性
- 保证下流服务接收的稳定性。均匀下发
缺点
- 我觉得没有缺点。非要鸡蛋里挑骨头那我只能说漏桶容量是个短板
实现
controller
@RequestMapping(value = "/startLoutong",method = RequestMethod.GET)
public Map<String,Object> startLoutong(@RequestParam Map<String, Object> paramMap) {
return testService.startLoutong(paramMap);
}
service
- 在service中我们通过redis的list的功能模拟出桶的效果。这里代码是实验室性质的。在真实使用中我们还需要考虑并发的问题
@Override
public Map<String, Object> startLoutong(Map<String, Object> paramMap) {
String redisKey = "qpsList";
Integer times = 100;
if (paramMap.containsKey("times")) {
times = Integer.valueOf(paramMap.get("times").toString());
}
Long size = redisTemplate.opsForList().size(redisKey);
if (size >= times) {
throw new RuntimeException("qps refuse request");
}
Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);
if (aLong > times) {
//为了防止并发场景。这里添加完成之后也要验证。 即使这样本段代码在高并发也有问题。此处演示作用
redisTemplate.opsForList().trim(redisKey, 0, times-1);
throw new RuntimeException("qps refuse request");
}
Map<String, Object> map = new HashMap<>();
map.put("success", "success");
return map;
}
下游消费
@Component
public class SchedulerTask {
@Autowired
RedisTemplate redisTemplate;
private String redisKey="qpsList";
@Scheduled(cron="*/1 * * * * ?")
private void process(){
//一次性消费两个
System.out.println("正在消费。。。。。。");
redisTemplate.opsForList().trim(redisKey, 2, -1);
}
}
测试
- 我们还是通过50并发循环10次访问。我们可以发现只有在一开始能达到比较高的吞吐量。在随后桶的容量满了之后。而下游水滴速率比上游请求速率慢的情况下。只能以下游恒定的速度接收访问。
- 他的问题也暴露得很明显。针对时间窗口的不足漏桶进行的不足,但是仍是不足。无法彻底避免请求溢出的问题。
- 请求溢出本身就是一种灾难性的问题。所有的算法目前都没有解决这个问题。只是在减缓他带来的问题
令牌桶算法
- 令牌桶和漏桶法是一样的。只不过将桶的作用方向改变了一下。
- 漏桶的出水速度是恒定的,如果流量突然增加的话我们就只能拒绝入池
- 但是令牌桶是将令牌放入桶中,我们知道正常情况下令牌就是一串字符当桶满了就拒绝令牌的入池,但是面对高流量的时候正常加上我们的超时时间就留下足够长的时间生产及消费令牌了。这样就尽可能地不会造成请求的拒绝
- 最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量
public Map<String, Object> startLingpaitong(Map<String, Object> paramMap) {
String redisKey = "lingpaitong";
String token = redisTemplate.opsForList().leftPop(redisKey).toString();
//正常情况需要验证是否合法,防止篡改
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("令牌桶拒绝");
}
Map<String, Object> map = new HashMap<>();
map.put("success", "success");
return map;
}
@Scheduled(cron="*/1 * * * * ?")
private void process(){
//一次性生产两个
System.out.println("正在消费。。。。。。");
for (int i = 0; i < 2; i++) {
redisTemplate.opsForList().rightPush(redisKey, i);
}
}
原文链接:https://juejin.cn/post/6967704472109711367
相关推荐
- Spring Boot3 连接 Redis 竟有这么多实用方式
-
各位互联网大厂的后端开发精英们,在日常开发中,想必大家都面临过系统性能优化的挑战。当系统数据量逐渐增大、并发请求不断增多时,如何提升系统的响应速度和稳定性,成为了我们必须攻克的难题。而Redis,这...
- 隧道 ssh -L 命令总结 和 windows端口转发配置
-
摘要:隧道ssh-L命令总结和windows端口转发配置关键词:隧道、ssh-L、端口转发、网络映射整体说明最近在项目中,因为内网的安全密级比较高,只能有一台机器连接内网数据库,推送...
- 火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer
-
火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer综上所述,面试遇到的所有问题,整理成了一份文档,希望大家能够喜欢!!Java面试题分享(Java中高级核心知识全面解析)一、J...
- 「第五期」游服务器一二三面 秋招 米哈游
-
一面下午2点,35分钟golang内存模型golang并发模型golanggc原理过程channel用途,原理redis数据结构,底层实现跳跃表查询插入复杂度进程,线程,协程kill原理除了kil...
- RMQ——支持合并和优先级的消息队列
-
业务背景在一个项目中需要实现一个功能,商品价格发生变化时将商品价格打印在商品主图上面,那么需要在价格发生变动的时候触发合成一张带价格的图片,每一次触发合图时计算价格都是获取当前最新的价格。上游价格变化...
- Redis 中的 zset 为什么要用跳跃表,而不是B+ Tree 呢?
-
Redis中的有序集合使用的是一种叫做跳跃表(SkipList)的数据结构来实现,而不是使用B+Tree。本文将介绍为什么Redis中使用跳跃表来实现有序集合,而不是B+Tree,并且探讨跳跃表...
- 一文让你彻底搞懂 WebSocket 的原理
-
作者:木木匠转发链接:https://juejin.im/post/5c693a4f51882561fb1db0ff一、概述上一篇文章《图文深入http三次握手核心问题【思维导图】》我们分析了简单的一...
- Redis与Java整合的最佳实践
-
Redis与Java整合的最佳实践在这个数字化时代,数据处理速度决定了企业的竞争力。Redis作为一款高性能的内存数据库,以其卓越的速度和丰富的数据结构,成为Java开发者的重要伙伴。本文将带你深入了...
- Docker与Redis:轻松部署和管理你的Redis实例
-
在高速发展的云计算时代,应用程序的部署和管理变得越来越复杂。面对各种操作系统、依赖库和环境差异,开发者常常陷入“在我机器上能跑”的泥潭。然而,容器化技术的兴起,尤其是Docker的普及,彻底改变了这一...
- Java开发中的缓存策略:让程序飞得更快
-
Java开发中的缓存策略:让程序飞得更快缓存是什么?首先,让我们来聊聊什么是缓存。简单来说,缓存是一种存储机制,它将数据保存在更快速的存储介质中,以便后续使用时能够更快地访问。比如,当你打开一个网页时...
- 国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer
-
字节跳动,先面了data部门,3面技术面之后hr说需要实习转正,拒绝,之后另一个部门捞起,四面技术面,已oc分享面经,希望对大家有所帮助,秋招顺利在文末分享了我为金九银十准备的备战资源库,包含了源码笔...
- “快”就一个字!Redis凭什么能让你的APP快到飞起?
-
咱们今天就来聊一个字——“快”!在这个信息爆炸、耐心越来越稀缺的时代,谁不希望自己手机里的APP点一下“嗖”就打开,刷一下“唰”就更新?谁要是敢让咱用户盯着个小圈圈干等,那简直就是在“劝退”!而说到让...
- 双十一秒杀,为何总能抢到?Redis功不可没!
-
一年一度的双十一“剁手节”,那场面,简直比春运抢票还刺激!零点的钟声一敲响,亿万个手指头在屏幕上疯狂戳戳戳,眼睛瞪得像铜铃,就为了抢到那个心心念念的半价商品、限量版宝贝。你有没有发现一个奇怪的现象?明...
- 后端开发必看!为什么说Redis是天然的幂等性?
-
你在做后端开发的时候,有没有遇到过这样的困扰:高并发场景下,同一个操作重复执行多次,导致数据混乱、业务逻辑出错?别担心,很多同行都踩过这个坑。某电商平台就曾因订单创建接口在高并发时不具备幂等性,用户多...
- 开发一个app需要哪些技术和工具
-
APP开发需要一系列技术和工具的支持,以下是对这些技术的清晰归纳和分点表示:一、前端开发技术HTML用于构建页面结构。CSS用于样式设计和布局。JavaScript用于页面交互和逻辑处理。React...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)