高并发系统有三大特征:限流、缓存和熔断,所以限流已经成为当下系统开发中必备的功能了。那么,什么是限流?如何实现限流?使用 Redis 能不能实现限流?接下来我们一起来看。

1.什么是限流?

限流是指在各种应用场景中,通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制,以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性,并确保服务质量。

使用限流有以下几个好处:

  1. 保护系统稳定性:过多的并发请求可能导致服务器内存耗尽、CPU 使用率饱和,从而引发系统响应慢、无法正常服务的问题。
  2. 防止资源滥用:确保有限的服务资源被合理公平地分配给所有用户,防止个别用户或恶意程序过度消耗资源。
  3. 优化用户体验:对于网站和应用程序而言,如果任由高并发导致响应速度变慢,会影响所有用户的正常使用体验。
  4. 保障安全:在网络层面,限流有助于防范 DoS/DDoS 攻击,降低系统遭受恶意攻击的风险。
  5. 运维成本控制:合理的限流措施可以帮助企业减少不必要的硬件投入,节省运营成本。

2.限流常见算法

限流的常见实现算法有以下几个:

  1. 计数器算法:将时间周期划分为固定大小的窗口(如每分钟、每小时),并在每个窗口内统计请求的数量。当窗口内的请求数达到预设的阈值时,后续请求将被限制。时间窗口结束后,计数器清零。

    • 优点:实现简单,易于理解。
    • 缺点:在窗口切换时刻可能会有突刺流量问题,即在窗口结束时会有短暂的大量请求被允许通过。
  2. 滑动窗口算法:改进了计算器算法(固定窗口算法)的突刺问题,将时间窗口划分为多个小的时间段(桶),每个小时间段有自己的计数器。随着时间流逝,窗口像滑块一样平移,过期的小时间段的计数会被丢弃,新时间段加入计数。所有小时间段的计数之和不能超过设定的阈值。
    • 优点:更平滑地处理流量,避免了突刺问题。
    • 缺点:实现相对复杂,需要维护多个计数器。
  3. 漏桶算法:想象一个固定容量的桶,水(请求)以恒定速率流入桶中,同时桶底部有小孔让水以恒定速率流出。当桶满时,新来的水(请求)会被丢弃。此算法主要用来平滑网络流量,防止瞬时流量过大。
    • 优点:可以平滑突发流量,保证下游系统的稳定。
    • 缺点:无法处理突发流量高峰,多余的请求会被直接丢弃。
  4. 令牌桶算法:与漏桶相反,有一个固定速率填充令牌的桶,令牌代表请求许可。当请求到达时,需要从桶中取出一个令牌,如果桶中有令牌则允许请求通过,否则拒绝。桶的容量是有限的,多余的令牌会被丢弃。
    • 优点:既能平滑流量,又能处理一定程度的突发流量(因为令牌可以累积)。
    • 缺点:需要精确控制令牌生成速度,实现较漏桶复杂。

3.使用Redis实现限流

使用 Redis 也可以实现简单的限流,它的常见限流方法有以下几种实现:

  1. 基于计数器和过期时间实现的计数器算法:使用一个计数器存储当前请求量(每次使用 incr 方法相加),并设置一个过期时间,计数器在一定时间内自动清零。计数器未到达限流值就可以继续运行,反之则不能继续运行。
  2. 基于有序集合(ZSet)实现的滑动窗口算法:将请求都存入到 ZSet 集合中,在分数(score)中存储当前请求时间。然后再使用 ZSet 提供的 range 方法轻易的获取到 2 个时间戳内的所有请求,通过获取的请求数和限流数进行比较并判断,从而实现限流。
  3. 基于列表(List)实现的令牌桶算法:在程序中使用定时任务给 Redis 中的 List 添加令牌,程序通过 List 提供的 leftPop 来获取令牌,得到令牌继续执行,否则就是限流不能继续运行。

了解了以上概念后,接下来我们来看具体的实现。

3.1 计数器算法

此方法的实现思路是:使用一个计数器存储当前请求量(每次使用 incr 方法相加),并设置一个过期时间,计数器在一定时间内自动清零,从而实现限流。

它的具体操作步骤如下:

  1. 使用 Redis 的计数器保存当前请求的数量。
  2. 设置一个过期时间,使得计数器在一定时间内自动清零。
  3. 每次收到请求时,检查计数器当前值,如果未达到限流阈值,则增加计数器的值,否则拒绝请求。

具体实现代码如下:

import redis.clients.jedis.Jedis;

public class RedisRateLimiter {
private static final String REDIS_KEY = "request_counter";
private static final int REQUEST_LIMIT = 100; // 限流阈值
private static final int EXPIRE_TIME = 60; // 过期时间(秒) public boolean allowRequest() {
Jedis jedis = new Jedis("localhost"); try {
Long counter = jedis.incr(REDIS_KEY);
if (counter == 1) {
// 第一次设置过期时间
jedis.expire(REDIS_KEY, EXPIRE_TIME);
} if (counter <= REQUEST_LIMIT) {
return true; // 允许请求通过
} else {
return false; // 请求达到限流阈值,拒绝请求
}
} finally {
jedis.close();
}
} public static void main(String[] args) {
RedisRateLimiter rateLimiter = new RedisRateLimiter();
for (int i = 0; i < 110; i++) {
if (rateLimiter.allowRequest()) {
System.out.println("Request Allowed");
} else {
System.out.println("Request Denied (Rate Limited)");
}
}
}
}

在上述代码中,每次请求会通过 allowRequest() 方法判断是否达到限流阈值,如果未达到则允许通过,并递增计数器的值,否则拒绝请求。同时,第一次设置计数器的过期时间,使得计数器在指定的时间内自动清零。

PS:以上是一个简单的示例,实际应用中需要根据具体场景实现更复杂的限流逻辑,并考虑并发情况下的线程安全性等问题。

因为计算器算法有突刺问题,因此我们需要使用升级版的滑动窗口算法或其他限流算法来解决此问题。

3.2 滑动窗口算法

此方法的实现思路是:将请求都存入到 ZSet 集合中,在分数(score)中存储当前请求时间。然后再使用 ZSet 提供的 range 方法轻易的获取到 2 个时间戳内的所有请求,通过获取的请求数和限流数进行比较并判断,从而实现限流。

它的具体操作步骤如下:

  1. 使用有序集合(ZSet)来存储每个时间窗口内的请求时间戳,成员(member)表示请求的唯一标识,分数(score)表示请求的时间戳。
  2. 每次收到请求时,将请求的时间戳作为成员,当前时间戳作为分数加入到有序集合中。
  3. 根据有序集合的时间范围和滑动窗口的设置,判断当前时间窗口内的请求数量是否超过限流阈值。

具体实现代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple; import java.util.Set; public class RedisSlidingWindowRateLimiter { private static final String ZSET_KEY = "request_timestamps";
private static final int WINDOW_SIZE = 60; // 时间窗口大小(单位:秒)
private static final int REQUEST_LIMIT = 100; // 限流阈值 public boolean allowRequest() {
Jedis jedis = new Jedis("localhost");
long currentTimestamp = System.currentTimeMillis() / 1000; // 添加当前请求的时间戳到有序集合
jedis.zadd(ZSET_KEY, currentTimestamp, String.valueOf(currentTimestamp)); // 移除过期的请求时间戳,保持时间窗口内的请求
long start = currentTimestamp - WINDOW_SIZE;
long end = currentTimestamp;
jedis.zremrangeByScore(ZSET_KEY, 0, start); // 查询当前时间窗口内的请求数量
Set<Tuple> requestTimestamps = jedis.zrangeByScoreWithScores(ZSET_KEY, start, end);
long requestCount = requestTimestamps.size(); jedis.close(); // 判断请求数量是否超过限流阈值
return requestCount <= REQUEST_LIMIT;
} public static void main(String[] args) {
RedisSlidingWindowRateLimiter rateLimiter = new RedisSlidingWindowRateLimiter(); for (int i = 0; i < 110; i++) {
if (rateLimiter.allowRequest()) {
System.out.println("Request Allowed");
} else {
System.out.println("Request Denied (Rate Limited)");
}
}
}
}

在上述代码中,每次收到请求时,将当前请求的时间戳加入到有序集合中,并移除过期的请求时间戳,然后查询当前时间窗口内的请求数量,判断是否达到限流阈值。这样基于 Redis 的滑动窗口限流算法可以有效控制单位时间内的请求流量,避免系统被过多请求压垮。

3.3 令牌桶算法

此方法的实现思路是:在程序中使用定时任务给 Redis 中的 List 添加令牌,程序通过 List 提供的 leftPop 来获取令牌,得到令牌继续执行,否则就是限流不能继续运行。

① 添加令牌

在 Spring Boot 项目中,通过定时任务给 Redis 中的 List 每秒中添加一个令牌(当然也可以通过修改定时任务的执行时间来控制令牌的发放速度),具体实现代码如下:

@Configuration      // 1.注入到 IoC 中,启动程序时加载
@EnableScheduling // 2.开启定时任务
public class SaticScheduleTask {
@Autowired
private RedisTemplate redisTemplate;
// 3.添加定时任务
@Scheduled(fixedRate = 1000)
private void configureTasks() {
redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
}
}

② 获取令牌

令牌的获取代码如下:

public boolean allowRequest(){
Object result = redisTemplate.opsForList().leftPop("limit_list");
if(result == null){
return false;
}
return true;
}

在上述代码中,我们每次访问 allowRequest() 方法时,会尝试从 Redis 中获取一个令牌,如果拿到令牌了,那就说明没超出限制,可以继续执行,反之则不能执行。

课后思考

使用 Redis 实现限流有什么优缺点?为什么微服务中不会使用 Redis 实现限流?

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

百度面试:如何用Redis实现限流?的更多相关文章

  1. 基于Redis的限流系统的设计

    本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计:在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本.   1.概念 In computer netw ...

  2. 深入Redis漏斗限流

    漏斗限流是最常用的限流方法之一,漏斗流水的速率大于灌水的速率,漏斗就永远装不满,反之水就会溢出. 所以漏斗的剩余空间就代表当前行为可以持续进行的数量,水流出的速率代表系统允许该行为的最大频率. imp ...

  3. Envoy实现.NET架构的网关(五)集成Redis实现限流

    什么是限流 限流即限制并发量,限制某一段时间只有指定数量的请求进入后台服务器,遇到流量高峰期或者流量突增时,把流量速率限制在系统所能接受的合理范围之内,不至于让系统被高流量击垮.而Envoy可以通过e ...

  4. 面试官:来谈谈限流-RateLimiter源码分析

    RateLimiter有两个实现类:SmoothBursty和SmoothWarmingUp,其都是令牌桶算法的变种实现,区别在于SmoothBursty加令牌的速度是恒定的,而SmoothWarmi ...

  5. 库存秒杀问题-redis解决方案- 接口限流

    <?php/** * Created by PhpStorm. * redis 销量超卖秒杀解决方案 * redis 文档:http://doc.redisfans.com/ * ab -n 1 ...

  6. 限流(三)Redis + lua分布式限流

    一.简介 1)分布式限流 如果是单实例项目,我们使用Guava这样的轻便又高性能的堆缓存来处理限流.但是当项目发展为多实例了以后呢?这时候我们就需要采用分布式限流的方式,分布式限流可以以redis + ...

  7. 【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流

    分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...

  8. 分布式限流组件-基于Redis的注解支持的Ratelimiter

    原文:https://juejin.im/entry/5bd491c85188255ac2629bef?utm_source=coffeephp.com 在分布式领域,我们难免会遇到并发量突增,对后端 ...

  9. Redis除了做缓存--Redis做消息队列/Redis做分布式锁/Redis做接口限流

    1.用Redis实现消息队列 用命令lpush入队,rpop出队 Long size = jedis.lpush("QueueName", message);//返回存放的数据条数 ...

  10. Redis解读(4):Redis中HyperLongLog、布隆过滤器、限流、Geo、及Scan等进阶应用

    Redis中的HyperLogLog 一般我们评估一个网站的访问量,有几个主要的参数: pv,Page View,网页的浏览量 uv,User View,访问的用户 一般来说,pv 或者 uv 的统计 ...

随机推荐

  1. 力扣273(java)-整数转换英文表示(困难)

    题目: 将非负整数 num 转换为其对应的英文表示. 示例 1: 输入:num = 123输出:"One Hundred Twenty Three"示例 2: 输入:num = 1 ...

  2. 日志审计携手DDoS防护助力云上安全

    ​简介: 本文主要介绍日志审计结合DDoS防护保障云上业务安全的新实践. 日志审计携手DDoS防护助力云上安全 1 背景介绍 设想一下,此时你正在高速公路上开车去上班,路上还有其他汽车,总体而言,大家 ...

  3. [FAQ] Pytorch PytorchStreamReader failed reading zip archive

    比如:rm -rf ~/.cache/huggingface Tool:ChatAI Link:https://www.cnblogs.com/farwish/p/17290240.html

  4. 记因为 NVIDIA 显驱错误而让 WPF 应用启动闪退问题

    本文记录一个因为 NVIDIA 显卡驱动错误而让 WPF 应用启动闪退问题 表现是 WPF 应用程序,在启动时,立刻闪退.在事件管理器看到的异常代码是 0xC0000005(Access Violat ...

  5. WPF 下拉框选项做鼠标 Hover 预览效果

    本文来告诉大家如何在 WPF 中,在 下拉框 ComboBox 里面,鼠标移动到 ComboBoxItem 上时,自动触发对应的事件,用来预览此选项值.例如我在实现一个颜色下拉框,此时我可以通过点击下 ...

  6. NopCommerce 多数据库方案

    本文转自:http://www.cnblogs.com/YUTOUYUWEI/p/5538200.html 有时候一个项目需要连接多个数据库,以实现不同数据库的数据在同个项目的共享. 如果已经安装了n ...

  7. python入门_模块2

    0.collections模块 在内置数据类型(dict.list.set.tuple)的基础上,collections模块还提供了几个额外的数据类型:Counter.deque.defaultdic ...

  8. vue3早已具备抛弃虚拟DOM的能力了

    前言 jquery时代更新视图是直接对DOM进行操作,缺点是频繁操作真实 DOM,性能差.react和vue时代引入了虚拟DOM,更新视图是对新旧虚拟DOM树进行一层层的遍历比较,然后找出需要更新的D ...

  9. docker-compse 安装nginx 配置目录挂载

    一.新建一个启动服务的目录 mkdir /usr/local/docker/compose cd /usr/local/docker/compose 二.新建文件docker-compose.yml ...

  10. 飞桨PaddleLite架构研读

    一.架构全景图 二.源码详细解读 1. Lite体系下似乎有多种 op_desc/program_desc 的定义,之间的关系是什么?这样设计的背景和好处是什么? model_parser目录下,包含 ...