Python+Redis实现高并发系统限流:从算法原理到Redis模块实战

周予安
周予安
发布时间:2026-10-13 | 6人浏览过
原创

本文深入解析高并发系统下的四种核心限流算法:固定窗口、滑动窗口、令牌桶与漏桶,并结合Python与Redis提供伪代码实现。重点介绍Redis 4.0+提供的redis-cell模块及其CL.THROTTLE命令,通过原子操作简化限流逻辑,帮助开发者构建稳定、高性能的分布式系统。

限流的基本概念与必要性

保障高并发系统稳定运行的三大支柱为缓存、降级与限流。所谓限流,即对流量进行限制。鉴于服务器处理能力存在上限,若允许超出阈值的请求无限制进入,将引发不可控的系统故障。通过限流机制,当请求量超过设定阈值时,系统可选择排队等待或直接拒绝服务,从而在无法承受极高并发时,实现有损服务而非完全不可用。

以疫情期间口罩紧缺为例,广州政府推出预约服务,仅允许预约成功的市民前往指定药店购买少量口罩。这便是生活中限流概念的直观体现。希望读者在此期间做好个人防护,注意健康安全。

下文将分享接口限流的常见实现方式,部分算法使用Python结合Redis进行了粗略实现,核心在于图解说明,请仔细体会其原理。

固定窗口限流算法

固定窗口法

固定窗口法是限流算法中最基础的一种。例如,限制1分钟内请求数为100个,从当前时刻起算的一分钟内,请求上限即为100个。当这一分钟结束的瞬间,计数器归零并重新计算,如此循环往复。

伪代码实现


def can_pass_fixed_window(user, action, time_zone=60, times=30):
  """
  :param user: 用户唯一标识
  :param action: 用户访问的接口标识(即用户在客户端进行的动作)
  :param time_zone: 接口限制的时间段
  :param time_zone: 限制的时间段内允许多少请求通过
  """
  key = '{}:{}'.format(user, action)
  # redis_conn 表示redis连接对象
  count = redis_conn.get(key)
  if not count:
    count = 1
    redis_conn.setex(key, time_zone, count)
  if count < times:
    redis_conn.incr(key)
    return True

  return False

该方法虽然简单,但存在一个显著缺陷:无法应对两个时间窗口边界处的突发流量。如上图所示,若在计数器清零前1秒和清零后1秒分别涌入100个请求,短时间内服务器将接收200个请求,这可能导致系统崩溃。出现此问题的原因在于统计精度不足,为降低临界问题的影响,可采用滑动窗口法。

滑动窗口限流算法

滑动窗口法

滑动窗口法的核心在于,随着时间推移,时间窗口持续移动,并通过计数器不断维护窗口内的请求数量,从而确保任意时间段内的请求数不超过最大允许值。例如,当前时间窗口为0s至60s,请求数为40;10秒后,时间窗口变为10s至70s,请求数更新为60。

时间窗口的滑动与计数器维护可利用Redis的有序集合(sorted set)实现。score值使用毫秒时间戳表示,通过当前时间戳减去时间窗口大小即可计算窗口边界,根据score值进行范围筛选以圈定窗口;value值仅作为用户行为的唯一标识,同样使用毫秒时间戳即可。最后统计窗口内的请求数并进行判断。

伪代码实现


def can_pass_slide_window(user, action, time_zone=60, times=30):
  """
  :param user: 用户唯一标识
  :param action: 用户访问的接口标识(即用户在客户端进行的动作)
  :param time_zone: 接口限制的时间段
  :param time_zone: 限制的时间段内允许多少请求通过
  """
  key = '{}:{}'.format(user, action)
  now_ts = time.time() * 1000
  # value是什么在这里并不重要,只要保证value的唯一性即可,这里使用毫秒时间戳作为唯一值
  value = now_ts 
  # 时间窗口左边界
  old_ts = now_ts - (time_zone * 1000)
  # 记录行为
  redis_conn.zadd(key, value, now_ts)
  # 删除时间窗口之前的数据
  redis_conn.zremrangebyscore(key, 0, old_ts)
  # 获取窗口内的行为数量
  count = redis_conn.zcard(key)
  # 设置一个过期时间免得占空间
  redis_conn.expire(key, time_zone + 1)
  if not count or count < times:
    return True
  return False

尽管滑动窗口法避免了时间边界问题,但仍无法完美解决细粒度时间范围内请求过于集中的问题。例如,限制1分钟请求不超过60次,若所有请求集中在第59秒发送,滑动窗口的效果将大打折扣。为了使流量更加平滑,可采用更高级的令牌桶算法和漏桶算法。

令牌桶限流算法

令牌桶法

令牌桶算法思路清晰:以固定速率生成令牌,并将令牌放入容量固定的桶中,超过桶容量的令牌将被丢弃。每个请求到来时尝试获取一个令牌,只有获得令牌的请求才能被放行,未获得令牌的请求则被丢弃。

伪代码实现


def can_pass_token_bucket(user, action, time_zone=60, times=30):
  """
  :param user: 用户唯一标识
  :param action: 用户访问的接口标识(即用户在客户端进行的动作)
  :param time_zone: 接口限制的时间段
  :param time_zone: 限制的时间段内允许多少请求通过
  """
  # 请求来了就倒水,倒水速率有限制
  key = '{}:{}'.format(user, action)
  rate = times / time_zone # 令牌生成速度
  capacity = times # 桶容量
  tokens = redis_conn.hget(key, 'tokens') # 看桶中有多少令牌
  last_time = redis_conn.hget(key, 'last_time') # 上次令牌生成时间
  now = time.time()
  tokens = int(tokens) if tokens else capacity
  last_time = int(last_time) if last_time else now
  delta_tokens = (now - last_time) * rate # 经过一段时间后生成的令牌
  if delta_tokens > 1:
    tokens = tokens + tokens # 增加令牌
    if tokens > tokens:
      tokens = capacity
    last_time = time.time() # 记录令牌生成时间
    redis_conn.hset(key, 'last_time', last_time)

  if tokens >= 1:
    tokens -= 1 # 请求进来了,令牌就减少1
    redis_conn.hset(key, 'tokens', tokens)
    return True
  return False

令牌桶法限制的是请求的平均流入速率,其优势在于能够应对一定程度的突发请求,并在一定程度上保留流量来源特征,且实现难度不高,适用于大多数应用场景。

漏桶限流算法及其实现

漏桶算法

漏桶算法的思路与令牌桶算法略有不同。可将请求视为水流,水流可以任意速率流入漏桶,同时漏桶以固定速率将水流出。若流入速度过快导致水桶溢出,溢出的请求将被丢弃。

从上图可以看出,漏桶算法的特点是:不限制请求流入的速率,但限制请求流出的速率。这样可以将突发流量整形为稳定的流量,避免超频现象。

关于漏桶算法的实现方式,有一点值得注意。在浏览相关资料时发现,网上大多数关于漏桶算法的伪代码实现仅实现了部分逻辑。

根据维基百科,漏桶算法的理论实现分为基于meter和基于queue两种,具体思路有所不同,此处简要介绍。

基于meter的漏桶

基于meter的实现相对简单,其核心是一个计数器。当有消息需要发送时,检查计数器是否充足;若计数器未满,则处理该消息;若计数器不足以发送消息,则该消息将被丢弃。

那么计数器如何生成呢?基于meter形式的计数器即为发送频率。例如,设置频率为不超过5条/秒,则计数器值为5。在一秒内,每发送一条消息计数器减一,当发送第6条消息时,计数器不足,该消息将被丢弃。

这种实现方式与前述固定窗口法类似,只是时间粒度更小,此处不再提供伪代码。

基于queue的漏桶

基于queue的实现较为复杂,但原理简单。它同样包含一个计数器,但该计数器不表示速率限制,而是表示队列(queue)的大小。当有消息需要发送时,检查队列中是否有空位;若有,则将消息放入队列,队列以FIFO形式提供服务;若队列已满,消息将被丢弃。

在消息放入队列后,还需维护一个定时器,其周期即为设置的频率周期。例如,设置频率为5条/秒,则定时器周期为200ms。定时器每200ms从队列中获取一次消息,若有消息则发送,若无则轮空。

需注意,网上许多关于漏桶算法的伪代码实现仅实现了水流入桶的部分,而未实现关键的水从桶中漏出的部分。若仅实现前半部分,其实与令牌桶算法并无太大区别。

若觉得上述算法实现过于复杂,强烈建议尝试使用redis-cell模块!

Redis模块redis-cell实战

redis-cell

Redis 4.0提供了一个限流Redis模块,名为redis-cell。该模块同样采用漏桶算法,并提供了原子的限流指令。借助此模块,限流问题变得非常简单。该模块需单独安装,网上安装教程众多,其核心指令仅有一个:

CL.THROTTLE

CL.THROTTLE user123 15 30 60 1 ▲ ▲ ▲ ▲ ▲ | | | | └───── apply 1 operation (default if omitted) 每次请求消耗的水滴 | | └──┴─────── 30 operations / 60 seconds 漏水的速率 | └───────────── 15 max_burst 漏桶的容量 └─────────────────── key “user123” 用户行为

执行上述命令后,Redis将返回如下信息:

> cl.throttle laoqian:reply 15 30 60 1) (integer) 0 # 0 表示允许,1表示拒绝 2) (integer) 16 # 漏桶容量 3) (integer) 15 # 漏桶剩余空间left_quota 4) (integer) -1 # 如果拒绝了,需要多长时间后再试(漏桶有空间了,单位秒) 5) (integer) 2 # 多长时间后,漏桶完全空出来(单位秒)

借助上述Redis模块,即可轻松应对大多数限流场景。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表