说说常见的限流算法及如何使用Redisson实现多机限流

说说常见的限流算法及如何使用Redisson实现多机限流

本文介绍了四种常见的限流算法及其实现方式。固定窗口限流简单但存在临界突刺问题;滑动窗口通过时间片滑动解决突刺问题,但滑动单位选择困难;漏桶算法以固定速率处理请求,能削峰缓冲但不够灵活;令牌桶算法允许突发流量,并发性能更好但时间单位选择仍需考量。实现层面,单机限流可使用Guava的RateLimiter,分布式限流推荐Redisson或网关层工具如Sentinel。提供了Redisson的配置示例和两种限流设置方式,建议采用基于Duration的新API实现更简洁的限流控制。

限流算法介绍

1、固定窗口限流

在单位时间内允许部分操作。

就比如,单位时间(窗口)为1小时,一小时内只能有100个用户能访问,如果在一小时内就已经达到了次数上限100,那么这时不论来多少请求都会被拒绝掉,也就是说1小时内设定了多少次就是只能有多少次,只有到1小时才会重置次数。但是它有一个突刺问题,例如在前59分钟都没有一个用户进行访问,在59分钟的时候100个用户同时访问,然后1小时01分时候又有100个用户都访问了,那就是2分钟有200个用户同时访问,可能瞬间通过200个请求,在两个窗口的临界突刺,服务器可能就会有流量高峰危险。

优点:实现简单

缺点:突刺问题

2、滑动窗口限流

在单位时间内允许部分操作,不过这个单位时间是滑动的,需要指定一个滑动时间单位(时间片)。

滑动窗口解决了固定窗口的突刺问题。滑动窗口是持续滑动,逐时间片淘汰旧数据的过程,它不会像固定窗口一样在固定的时间点重置次数,而是持续淘汰旧数据,因此能更精确地控制任意连续内的请求速率。

比如,单位时间(窗口)是1小时,指定滑动单位是1分钟,那么在59分钟时它的限流窗口是59分~1小时59分,经过2分钟它的限流窗口就是1小时01分~2小时01分,这个时间段还是就只接收100个请求,如果有超过第100个的请求访问,超过的访问请求都会被拒绝掉;如果每2分钟都有1个旧的请求数据淘汰,经过2分钟这时只有99个数据就可以允许有1个新的请求进入。

优点:解决突刺问题

缺点:滑动窗口的限流效果与滑动单位有关,滑动单位小效果好,不过往往很难选到一个合适的滑动单位

3、漏桶算法限流(推荐)

请求桶大小固定,以固定的速率处理请求,当请求桶里面满了之后会拒绝请求。

漏桶算法限流的基本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝。

大致的漏桶限流规则如下:

1、进水口(对应客户端请求)以任意速率流入进入漏桶。

2、漏桶的容量是固定的,出水(放行)速率也是固定的。

3、漏桶容量是不变的,如果处理速度太慢,桶内水量会超出了桶的容量,则后面流入的水滴会溢出,表示请求拒绝。

以一定速率处理请求,有大量流量进入时,会发生溢出,从而限流保护服务可用(也就是削峰),不至于直接请求到服务器,缓冲压力(也就是缓冲)。

优点:一定程度上应对流量突刺,以恒定的速率处理请求,保证服务器安全

缺点:漏桶出口的速度固定,不能灵活的应对后端能力提升。比如,通过动态扩容,后端流量从100QPS提升到1000QPS,漏桶没有办法。

4、令牌桶算法限流(推荐)

管理员提前生成一批令牌,比如每秒 10 个令牌,当用户要操作前,先去拿到一个令牌,有令牌的人就有资格执行操作、能同时执行操作,而拿不到令牌的就拒绝请求得等着。当然了令牌的数量也是有上限的,令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。

令牌桶的大小固定,令牌的产生速度固定可以自己设置,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情况);每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。

令牌桶限流大致的规则如下:

1、进水口按照某个速度,向桶中放入令牌。

2、令牌的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。

3、如果令牌的发放速度,慢于请求到来速度,桶内就无牌可领,请求就会被拒绝。

优点:比漏桶更高效,可以方便地应对突发出口流量,能够并发处理同时的请求,并发性能会更高

缺点还是时间单位选取的问题,令牌的产生速度、消耗令牌的速度对应时间选多少合适

限流实现

单机限流

使用谷歌提供的限流工具Guava RateLimiter第三方库实现,对单位时间的请求数量进行限制。

示例代码:

import com.google.common.util.concurrent.RateLimiter; public static void main(String[] args) { // 每秒限流10个请求 RateLimiter limiter = RateLimiter.create(10.0); while (true) { if (limiter.tryAcquire()) { // 处理请求 } else { // 超过流量限制,需要做何处理 } } } 

多机限流

1、使用操作 Redis 的工具库Redisson内置了一个限流工具,分布式限流实现,把用户的使用频率等数据放到一个集中的存储进行统计,这样无论用户的请求落到了哪台服务器,都以集中存储中的数据为准。

2、在网关层集中进行限流和统计,如 Sentinel、Spring Cloud Gateway

Redisson限流实现

参考官方文档:https://github.com/redisson/redisson

首先引入Redisson依赖:

<!-- redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>4.1.0</version> </dependency>

还需要有redis的依赖和application.yml配置

<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
# Redis 配置 redis: database: 0 host: localhost port: 6379 timeout: 5000 # password: 123456

然后写个RedissonConfig配置类:

/** * Redisson 配置(初始化RedissonClient对象单例) */ @Configuration @ConfigurationProperties(prefix = "spring.redis") @Data public class RedissonConfig { private Integer database; private String host; private Integer port; // 如果你的redis默认没有设置密码则不用写 private String password; @Bean public RedissonClient getRedissonClient() { // 1.创建配置对象 Config config = new Config(); config.useSingleServer() .setDatabase(database) .setAddress("redis://" + host + ":" + port); // 2.创建Redisson实例 return Redisson.create(config); } }

再写个通用的RedissonManager类,便于使用redisson限流功能:

查看源码发现trySetRate方法的图中参数用法在新版的Redisson中是被弃用的,现在推荐使用RateIntervalUnit的替代方案org.redisson.api.RateType和Duration

第一种是:

rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1));

第二种是:

rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.valueOf("SECONDS"));

推荐使用第一种方式,因为:

1、使用 Duration 更符合 Java 8+ 的日期时间 API 设计理念

2、代码更简洁明了

3、这是 Redisson 官方推荐的新用法

因此,最终代码为

/** * 提供RedissonLimiter限流基础服务 */ @Service public class RedissonManager { @Resource private RedissonClient redissonClient; public void doRateLimit(String key){ // 创建一个限流器 RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); // 设置限流器速率为每秒2个请求 rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1)); // 每当一个请求过来,尝试获取一个令牌 boolean canOp = rateLimiter.tryAcquire(1); if (!canOp){ //TOO_MANY_REQUEST_ERROR(42900, "请求过于频繁") throw new BusinessException(ErrorCode.TOO_MANY_REQUEST_ERROR, "操作太频繁,请稍后再试"); } } }

最后在controller中使用它:

/** * 获取当前登录用户 * * @param request * @return */ @Override public User getLoginUser(HttpServletRequest request) { // 先判断是否已登录 // 用户登录态键String USER_LOGIN_STATE = "user_login"; Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User currentUser = (User) userObj; if (currentUser == null || currentUser.getId() == null) { //NOT_LOGIN_ERROR(40100, "未登录") throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } // 从数据库查询(追求性能的话可以注释,直接走缓存) long userId = currentUser.getId(); currentUser = this.getById(userId); if (currentUser == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } return currentUser; }
@Resource private RedissonManager redissonManager; User loginUser = userService.getLoginUser(request); String id = String.valueOf(loginUser.getId()); redissonManager.doRateLimit("generateByAi_" + id);

Read more

《算法闯关指南:优选算法--位运算》--34.判断字符是否唯一,35.丢失的数字

《算法闯关指南:优选算法--位运算》--34.判断字符是否唯一,35.丢失的数字

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 位运算基础前置知识 * 34. 判断字符是否唯一 * 解法(位图的思想): * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 35. 丢失的数字 * 解法(位运算): * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:优选算法:剖析动态规划、二分法等高效策略,学会寻找“最优解”。 递归与回溯:掌握问题分解与状态回退,攻克组合、排列等难题。 贪心算法:

By Ne0inhk
哈希表的介绍和使用

哈希表的介绍和使用

今天,我们来介绍的是哈希表,哈希表主要用于对数据的出现次数统计,查重。利用的容器主要有vector、map/set、ordered_map/ordered_set等。   下面我们来看几道例题: class Solution { public:     vector<int> twoSum(vector<int>& nums, int target) {         unordered_map<int,int> hash;         for(int i=0;i<nums.size();i++){             int x=target-nums[i];             if(hash.

By Ne0inhk
【数据结构-初阶】详解线性表(5)---队列

【数据结构-初阶】详解线性表(5)---队列

🎈主页传送门:良木生香 🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》 🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离 上期回顾:在上一篇文章(【数据结构-初阶】详解栈和队列(1)---栈)中我们讲到了在顺序表与链表之外的另一种线性表---栈,知道了这是一种具有先进后出和后进先出特点的数据结构,既然有先进后出,那么肯定就有先进先出的数据结构,所以这就是我们今天要讲的------队列 一、队列的概念 既然我们想要实现先进先出的效果,那肯定就不像栈那样有一端是堵起来的,想必应该是两端都开口吧。嗯,事实确实如此。 队列:是只允许在一端进行数据的插入操作,在另一端进行数据的删除操作的一种特殊的线性表,其具有先进先出FIFO(first in first out)的结构特点. 入队列:进行插入操作的一端叫做队尾 出队列:进行删除操作的一端叫做队头 下面是队列的示意图: 名字叫做队列,其实就像我们排队一样,先排的人先得服务,后排的人后得到服务,在队列中,先进来的元素先得到操作,

By Ne0inhk
【算法】滑动窗口(一)-长度最小的子数组

【算法】滑动窗口(一)-长度最小的子数组

目录 一、题目介绍 二、算法原理 1.排必然非结果情况 1.1.2区域 (1)预证区 (2)已证区 2.滑动窗口 三、提交代码 一、题目介绍 209. 长度最小的子数组 - 力扣(LeetCode) 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 示例 1: 输入:target = 7, nums = [2,3,

By Ne0inhk