当前位置:首页 > 问答 > 正文

Redis限流怎么搞,简单说说那些策略和用法你得知道

说到Redis限流,说白了就是控制一个系统在单位时间内能处理的请求数量,防止被突如其来的流量冲垮,就像热门景点要限制入园人数一样,不然大家都挤进去,谁也玩不好,Redis因为速度快、支持复杂数据结构,天生就适合干这个实时计数和判断的活儿。

下面聊聊几种常见的策略和用法,咱们用开闸放水的例子来理解。

固定窗口计数器 这是最简单直接的法子,想象一下,我们把时间切成一个个一分钟的窗口,每个窗口内,我们用一个Redis键来计数,rate_limit:api1:202406011300(表示2024年6月1日13点00分这个分钟窗口),每当一个请求进来,我们就给这个键的值加1(使用 INCR 命令),如果这个值超过了我们设定的阈值(比如一分钟100次),就把后续的请求都拒之门外,等到下一分钟,键名变了,202406011301,计数器又从0开始,新窗口的请求又可以进来了。

  • 优点:实现超级简单,Redis操作少,性能消耗低。
  • 缺点不够平滑,有临界问题,比如在1分59秒瞬间来了100个请求,2分00秒又瞬间来了100个请求,虽然两个单独的窗口都没超限,但在1分59秒到2分00秒这短短1秒内,系统实际承受了200个请求,可能还是会被打垮,这就像在窗口切换的瞬间,有人钻了空子。

滑动窗口 为了解决固定窗口的临界问题,滑动窗口出现了,它把时间窗口划分得更细,比如把一分钟分成6个10秒的小格子,它记录的不是整个大窗口的请求数,而是最近N个小格子内的请求总和。

具体做的时候,我们可以用一个有序集合(ZSET)来实现,集合的成员可以是请求的唯一标识(比如UUID),分数是请求到达的时间戳,当一个请求进来时:

  • ZREMRANGEBYSCORE 命令清理掉所有超过一分钟(当前时间戳减60000毫秒)的旧成员。

  • ZCARD 命令计算当前集合中还剩多少成员,这就是最近一分钟内的请求总数。

  • 如果总数小于阈值,就用 ZADD 命令把这个新请求加进去;如果已经达到阈值,就拒绝。

  • 优点:比固定窗口平滑得多,能更精确地控制单位时间内的请求量,有效避免临界问题。

  • 缺点:因为要存储每个请求的时间戳(或者至少是计数单元),并且要频繁地清理旧数据,比固定窗口消耗更多内存和CPU,时间粒度划分得越细,就越精确,但开销也越大。

漏桶算法 这个算法思想很形象,想象一个底部有固定大小出水口的桶,请求像水一样,不管以多快的速率流入桶中,桶都以一个恒定的、预先定义好的速率向外漏出(被处理),如果水流太快,桶满了,多出来的水(请求)就会被直接丢弃或排队等待。

Redis限流怎么搞,简单说说那些策略和用法你得知道

用Redis实现漏桶,我们可以用一个键来记录桶里当前的水量,另一个键记录最后一次漏水的时间,每次请求进来:

  • 先计算从上一次漏水到现在,桶里应该漏掉了多少水(当前时间减去上次时间,乘以漏水速率),并更新当前水量(减去漏掉的水,但不能小于0)。

  • 然后判断,如果当前水量加上本次请求的“水量”(通常是1)小于桶的容量,就允许请求通过,并更新水量;否则就拒绝。

  • 优点能够非常平滑地限制请求的处理速率,无论上游流量多么不均匀,下游都能以恒定速度处理,非常适合保护下游系统,比如限制调用某个第三方API的频率。

  • 缺点无法应对突发流量,即使系统此刻有充足的处理能力,因为出水速率是固定的,突发的大量请求还是会被强行限制成匀速,可能导致响应变慢,它是一种“匀速”限流。

令牌桶算法 这是漏桶的一个变种,也更常用,想象一个以固定速率往桶里放令牌的装置,每个请求要想被处理,必须先从桶里拿到一个令牌,如果桶里有令牌,请求就可以立即通过,并消耗掉一个令牌,如果桶里没令牌了,请求就得等待或者被拒绝,如果一段时间没有请求,令牌会在桶里堆积起来,最多堆到桶的容量上限,这样,当突发流量来时,可以一次性消耗掉之前积攒的令牌,从而允许一定程度的突发流量通过。

Redis限流怎么搞,简单说说那些策略和用法你得知道

Redis实现令牌桶,类似漏桶,用一个键存当前令牌数,一个键存上次补充令牌的时间,请求进来时:

  • 先计算从上一次更新到现在,应该新产生多少令牌(当前时间减去上次时间,乘以令牌生成速率),并更新当前令牌数(加上新令牌,但不能超过桶容量)。

  • 如果当前令牌数大于0,就允许请求通过,并将令牌数减1;否则拒绝。

  • 优点既能够将平均速率限制在预定值,又允许一定程度的突发流量,这更符合很多实际业务场景,比如秒杀开始的前几秒,系统可以应对短暂的流量高峰。

  • 缺点:实现上比固定窗口稍复杂。

实际使用中的要点

  • 选择策略:根据你的场景来选,要简单粗暴且能接受临界问题,用固定窗口;要精确平滑,用滑动窗口;要保护下游系统匀速,用漏桶;要允许合理突发,用令牌桶,令牌桶是综合表现最好的。
  • 分布式一致性:在分布式环境下,所有服务实例都连接同一个Redis,这样限流就是集群级别的,如果你用多个Redis实例,就需要更复杂的方案来保证一致性。
  • 限流key的设计:key要能区分不同的限流维度,比如按用户(rate_limit:user123)、按接口(rate_limit:/api/v1/order)或者两者结合(rate_limit:user123:/api/v1/order)。
  • 处理被限流的请求:直接返回HTTP 429(Too Many Requests)状态码是标准做法,也可以根据业务需求,让请求排队等待,或者降级返回一个默认值。

现在有很多现成的库帮你实现了这些算法,比如Redis官方的RedisCell模块就直接提供了基于令牌桶的限流命令,一行命令就能搞定,不用自己写Lua脚本去保证原子性了,非常方便,但在用之前,理解这些底层策略的思想,能让你在关键时刻做出最合适的选择。