Redis限流方案

限流简介

限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时,如何阻止计划外的请求继续对系统施压,是一个需要重视的问题。

除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求,比如在UGC社区,用户的发帖、回复、点赞等行为都要严格受控,一般要严格限定某行为在规定时间内允许的次数,超过了次数那就是非法行为。对于非法行为,业务必须规定适当的惩处策略。

简单限流

首先我们来看一个常见的简单限流策略。系统要限定用户某个行为在指定的时间里只能发生N次,如何使用Redis的数据结构来实现这个限流功能。

首先,这个接口定义如下

#指定用户use_id的某个行为action_key在特定的时间内period只允许发生一定的次数
int max_count;
boolean is_action_allowed(user_id,action_key,period,max_count){return true;
}
# 调用这个接口,一分钟内只允许最多回复5个贴子
can_reply = is_action_allowed("user1","reply",60,5);
if(can_reply) do();
else return ;

解决方案
这个限流需求中存在一个滑动时间窗口,在zset数据结构中提供了zremrangebyscore key-name min max指令用于移除有序集合中分值介于min和max之间的元素集合。因此可以使用行为发生的时间戳作为zset集合中元素的score,时间戳越大,相应score越高。 而且我们只需要保留这个时间窗口,窗口之外的数据都可以砍掉,从而节省内存。

示意图如下
在这里插入图片描述
代码如下:

package example;import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;public class SimpleRateLimiter {private Jedis jedis;public SimpleRateLimiter(Jedis jedis) {this.jedis = jedis;}public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {String key = String.format("hist:%s:%s", userId, actionKey);long nowTs = System.currentTimeMillis();Pipeline pipe = jedis.pipelined();pipe.multi();pipe.zadd(key, nowTs, "" + nowTs);pipe.zremrangeByScore(key, 0, nowTs - period * 1000);Response<Long> count = pipe.zcard(key);pipe.expire(key, period + 1);pipe.exec();pipe.close();return count.get() <= maxCount;}public static void main(String[] args) {Jedis jedis = new Jedis();SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);for (int i = 0; i < 20; i++) {System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));}}
}

代码整体思路:每一个行为到来时,都维护一次时间窗口,将时间窗口外的记录全部清理掉,只保留窗口内的记录。zset集合中只有score值非常重要,value值没有特别意义。只需要保证他的唯一性即可。

因为这几个连续的Redis操作都是针对同一个key的,使用pipeline可以显著提升Redis存取效率。但是这种方案也有缺点,因为他要记录时间窗口内所有的行为记录。如果这个量很大,比如限定60s内操作不得超过100w次这样的参数,他是不适合做这样的限流的,因为会消耗大量的存储空间。

高级限流算法——漏洞限流

漏洞限流是最常用的限流方法之一,这个算法的灵感来源于漏斗的结构。漏斗的容量是有限的,如果将漏嘴堵住,然后一直往里面灌水,他就会变满,直至再也装不进去。如果将漏嘴放开,水就会往下流,流走一部分之后,就可以继续往里面灌水。如果漏嘴流水的速率大于灌水的速率。那么漏洞永远都不会装满。如果漏嘴流水速率小雨灌水速率,那么一旦漏斗满了,灌水就需要暂停并等待漏斗腾空。

所以,漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着系统允许该行为的最大频率。

单机漏斗算法:

package example;import java.util.HashMap;
import java.util.Map;public class FunnelRateLimiter {static class Funnel {//漏斗容量int capacity;//漏斗速率float leakingRate;//漏斗剩余容量int leftQuota;//滑动窗口的开始时间long leakingTs;public Funnel(int capacity, float leakingRate) {this.capacity = capacity;this.leakingRate = leakingRate;this.leftQuota = capacity;this.leakingTs = System.currentTimeMillis();}void makeSpace() {//获取当前时间long nowTs = System.currentTimeMillis();//当前时间-滑动窗口的开始时间= 滑动窗口的时长long deltaTs = nowTs - leakingTs;//滑动窗口的时长*漏水速率 = 滑动窗口内腾出的容量int deltaQuota = (int) (deltaTs * leakingRate);if (deltaQuota < 0) { // 间隔时间太长,整数数字过大溢出this.leftQuota = capacity;this.leakingTs = nowTs;return;}if (deltaQuota < 1) { // 腾出空间太小,最小单位是 1return;}//剩余容量 = 当前剩余容量+腾出的容量this.leftQuota += deltaQuota;//重置窗口开始时间this.leakingTs = nowTs;if (this.leftQuota > this.capacity) {this.leftQuota = this.capacity;}}boolean watering(int quota) {makeSpace();//剩余容量>所需容量if (this.leftQuota >= quota) {this.leftQuota -= quota;return true;}return false;}}private Map<String, Funnel> funnels = new HashMap<>();public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {String key = String.format("%s:%s", userId, actionKey);Funnel funnel = funnels.get(key);if (funnel == null) {funnel = new Funnel(capacity, leakingRate);funnels.put(key, funnel);}return funnel.watering(1); // 需要 1quota }
}

Funnel对象的make_space方法是漏斗算法的核心,其次在每次灌水前都会被调用以触发漏水,给漏斗腾出空间。能腾出多少空间取决于过去了多久以及流水的速率。Funnel对象占据的空间大小不再和行为的频率成正比,她的空间占用是一个常量。

分布式限流 Redis-Cell

Redis-Cell语法
在这里插入图片描述

127.0.0.1:6379> cl.throttle mytest 99 5 100 2
1) (integer) 0                        #0 表示成功, 1表示失败
2) (integer) 100                      # 令牌桶的容量
3) (integer) 98                       # 当前令牌桶的令牌数
4) (integer) -1                       # 成功时该值为-1,失败时表还需要等待多少秒可以有足够的令牌
5) (integer) 41                       # 预计多少秒后令牌桶会满

多次从令牌桶中取出数据

127.0.0.1:6379> cl.throttle mytest 99 5 100 40
1) (integer) 0
2) (integer) 100
3) (integer) 54
4) (integer) -1
5) (integer) 911
127.0.0.1:6379> cl.throttle mytest 99 5 100 40
1) (integer) 0
2) (integer) 100
3) (integer) 14
4) (integer) -1
5) (integer) 1708
127.0.0.1:6379> cl.throttle mytest 99 5 100 40
1) (integer) 1                      #失败,拒绝取出
2) (integer) 100
3) (integer) 14
4) (integer) 505                  # 取出失败,令牌桶还有14个令牌,还需505秒才能够取出
5) (integer) 1705

代码示例

package example;import io.rebloom.client.Client;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.Jedis;import java.util.ArrayList;
import java.util.List;public class RedisCellExample {private Jedis jedis;public RedisCellExample() {jedis = new Jedis("127.0.0.1", 6379);}public Boolean rush() {//对接口进行限流操作//检查令牌桶,返回的第一值是否为 0: 0-流量够,1-限流中String script = "return redis.call('cl.throttle',KEYS[1],ARGV[1],ARGV[2],ARGV[3],ARGV[4])";List<String> keys = new ArrayList<>();keys.add("redbag");String maxBurst = "99";  //漏洞容量String countPerPeriod = "10";String period = "100";String quantity = "10";List<String> values = new ArrayList<>();values.add(maxBurst);values.add(countPerPeriod);values.add(period);values.add(quantity);List<Integer> list = (List) jedis.eval(script, keys, values);if (!list.isEmpty() && list.get(0) == 0) {return true;} else {return false;}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/849465.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python基础语法(五):循环语句

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

企业公户验证API在Java、Python、PHP中的使用教程

在金融和商业领域&#xff0c;企业公户验证API是一种用于验证企业对公账户的真实性和合法性的技术解决方案。这种API通常由金融机构或第三方服务提供商提供&#xff0c;旨在帮助企业加快账户认证流程&#xff0c;提高效率&#xff0c;降低审核成本&#xff0c;并确保符合法规要…

苹果Safari怎么清理缓存?原来快速清除浏览器的历史记录那么容易

在数字化时代&#xff0c;互联网已经成为我们日常生活中不可或缺的一部分。我们使用各种设备&#xff0c;如智能手机、平板电脑和笔记本电脑来浏览网页、获取信息、娱乐和社交。而在这些设备中&#xff0c;iPhone无疑是最受欢迎的选择之一。iPhone搭载的Safari浏览器以其简洁的…

ui自动化中,鼠标操作

from selenium.webdriver import ActionChainsaction ActionChains(driver) # 然后把driver作为参数&#xff0c;实例化一个action对象 练习地址&#xff1a;https://sahitest.com/demo/ 悬停例子&#xff08;百度首页&#xff09; action.move_to_element(ele).perfor…

深入浅出服务网格(Service Mesh):现代微服务架构的护航者

什么是服务网格&#xff1f; 服务网格是一种专用于处理微服务间通信的基础设施层&#xff0c;通常以轻量级代理&#xff08;sidecar&#xff09;的形式部署在每个服务实例旁边。它主要负责以下几项任务&#xff1a; 服务发现&#xff1a;自动检测和注册服务实例&#xff0c;使…

Springboot注意点

1.Usermapper里加param注解 2.RequestParam 和 RequestBody的区别&#xff1a; RequestParam 和 RequestBody的区别&#xff1a; RequestParam 和 RequestBody 是Spring框架中用于处理HTTP请求的两个不同的注 get请求一般用url传参数&#xff0c;所以参数名和参数的值就在ur…

Type-C音频转接器方案

在数字化时代&#xff0c;音频设备作为我们生活中不可或缺的一部分&#xff0c;其连接方式的便捷性和高效性显得尤为重要。Type-C音频转接器&#xff0c;作为一种新型的音频连接解决方案&#xff0c;正逐渐走进我们的生活&#xff0c;以其独特的优势改变着我们的音频体验。 一、…

Vue Router 4与路由管理实战

title: Vue Router 4与路由管理实战 date: 2024/6/7 updated: 2024/6/7 excerpt: 这篇文章介绍了如何在Vue.js应用中利用Vue Router实现单页面应用的路由管理&#xff0c;包括配置路由、导航守卫的使用、路由懒加载以优化性能以及动态路由的实现方法&#xff0c;旨在提升用户体…

Python3 笔记:字符串的 strip()、lstrip()、rstrip()

1、strip() 方法用于移除字符串头尾指定的字符&#xff08;默认为空格&#xff09;或字符序列。 注意&#xff1a;该方法只能删除开头或是结尾的字符&#xff0c;不能删除中间部分的字符。 语法&#xff1a;str.strip([chars]) chars&#xff1a;移除字符串头尾指定的字符序…

JAVA多维动态数组

JAVA多维动态数组 一维数组数组大小 size()增加元素 add获取元素 get删除元素 remove修改元素 set输出 二维数组三维数组 ArrayList 类是一个可以动态修改的数组&#xff08;队列&#xff09;&#xff0c;与普通数组的区别就是它是没有固定大小的限制&#xff0c;提供了相关的添…

xiaolingcoding 图解网络笔记——基础篇

文章目录 参考一、网络模型有哪几层DMANAPI 机制二、键入网址到网页显示&#xff0c;期间发生了什么&#xff1f;1. HTTP2. DNS3. 协议栈4. TCP5. IP6. MAC7. 网卡8. 交换机9. 路由器10. 服务器 与 客户端的互相扒皮&#xff08;添加、删除头部信息&#xff09;参考图HTTP 请求…

Vue3【六】setup的使用和setup的返回值

Vue3【六】setup的使用和setup的返回值 setup函数的使用&#xff0c;和vue2的选项式不同 vue3的组合式使用的是setup函数 通过返回值将数据和方法传到页面 返回值也可以是一个箭头函数 setup先于 data和method执行所有无法读取到this和data&#xff0c;method的内容&#xff0c…

顶顶通呼叫中心中间件-asr录音路径修改(mod_cti基于FreeSWITCH)

顶顶通呼叫中心中间件-asr录音路径修改(mod_cti基于FreeSWITCH) 录音路径模板。如果不是绝对路径&#xff0c;会把这个路径追加到FreeSWITCH的recordings后面。支持变量&#xff0c;比如日期 ${strftime(%Y-%m-%d)}。最后一个录音文件路径会保存到变量 ${cti_asr_last_record_…

几个日期和时间相关的函数,用于格式化日期、时间和毫秒数

几个日期和时间相关的函数&#xff0c;用于格式化日期、时间和毫秒数。 javascript /** 返回年月日param {Date} date - 日期对象param {string} [splitor‘-’] - 分隔符&#xff0c;默认为’-’returns {string} - 格式化后的日期字符串&#xff0c;格式为YYYY-MM-DD */ exp…

Vue3实现当访问的路由不存在时跳转到404页面的方法详解

Vue3实现当访问的路由不存在时跳转到404页面的方法详解 一、前言1. 创建 404 组件2. 配置路由3. 使用 router-link 一、前言 在 Vue 3 中&#xff0c;你可以使用 Vue Router 来实现当访问的路由不存在时跳转到 404 页面。以下是详细的方法&#xff1a; 1. 创建 404 组件 首先…

03-3.2.4 双端队列

&#x1f44b; Hi, I’m Beast Cheng&#x1f440; I’m interested in photography, hiking, landscape…&#x1f331; I’m currently learning python, javascript, kotlin…&#x1f4eb; How to reach me --> 458290771qq.com 喜欢《数据结构》部分笔记的小伙伴可以订…

1348:【例4-9】城市公交网建设问题

【解题思路】 该题难点是要保存最小生成树的所有边&#xff0c;对于不同的求最小生成树的算法&#xff0c;有不同的方法来完成。 【参考代码】 prim算法 #include<bits/stdc.h> using namespace std; const int N 105; // 定义常量 N&#xff0c;表示数组大小 int n…

Vue3 时间格式化

1、基础使用 https://juejin.cn/post/6933548549321785352 2、template里面直接使用 $dayjs(info.patientBirthday).format("YYYY-MM-DD") 3、script里面使用 import { ref, reactive, getCurrentInstance } from "vue"; const { proxy } getCurren…

python的rolling_mean()函数

5.1 rolling_mean函数解释 pandas.rolling_mean(arg, window, min_periodsNone, freqNone, centerFalse, howNone, **kwargs)rolling_mean函数表示通过移动窗口求平均值&#xff0c;即用当前值和前[window]个数值取平均数&#xff0c;得到新的数值。 import pandas as pddata…

C语言详解(动态内存管理)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…