Reids限流

前言:

“限流”顾名思义就是限制流量,它也是保护高并发系统三个法宝之一(保护高并发三种方式:缓存、

降级、限流),我们都知道服务器的处理能力是有一定上限的,如果超出这个上限继续把请求放进来,

可能会发生很多不可预料的麻烦事的。通过限流可以在超出我们预先设置的请求量最大值就让剩余的

请求排队或者拒绝服务,这样就可以让系统在扛不住高并发的情况下做到"有损服务"而不是导致服务

器崩掉彻底无法服务。

举个例子:最近华为新手机mate40上架发行,由于手机发行的数量有限,市场黄牛过多存货抬高价

格的情况。华为官方为了解决消费者买不到手机,发出向每个华为直营店或网店派一定数量的手机

,只要去实体店或网店预约就能买到新款手机,不用都挤在官方网店或者指定单独地点够买,这就

是生活的限流。

固定窗口法

以我对限流算法理解,固定窗口法是限流里面最简单的,比如我想限制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

**注:**这个方法虽然是简单,但是有个大问题,就是无法应对两个时间边界的突发流量。

​ 因为我们这样统计出的精密度不够,所以为了将临界问题的影响减低,我们可以

​ 使用滑动窗口法。

滑动窗口法

滑动窗口法,以我的理解来说就是随着时间的变化,时间窗口也会持续移动,有一个计时器

不断的维护窗口里面的请求数量,这样就能保证在任何时间段内都能控制允许请求数量,不

会超过我们设置的最大请求量。例如:当前窗口是0s-60s,请求数是40,那10s之后的时间窗

口就变成了10s-70s,请求量就是60。

时间窗口是滑动和计数器可以使用redis的有序集合(stored set)来实现。score的值用毫秒

时间戳来表示,可以利用**“当前时间戳 - 时间窗口大小”**来计算出窗口的边界,然后根据score

的值做一个范围筛选就可以圈出一个窗口;value的值仅作为用户行为的唯一标准,也用毫秒

时间戳就行了,最后统计一下窗口内的请求数再判断就ok了。

伪代码(仅供参考具体功能实现应业务所需)

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次,请求都集中在59s时发送过来,这样滑动窗口的效果 就大打折扣。 为了使流量更加平滑,我们可以使用更加高级的令牌桶算法和漏桶算法。

令牌桶法

令牌桶方法的思路并没有那么复杂,我理解为:它是按照固定的速率生成令牌,再把令牌放在固定的

桶中,超出桶容量的令牌丢掉,每来一个请求就获取一次令牌,规定只要有令牌的请求才能通过并执

行,没有令牌的强求就丢弃。

在这里插入图片描述

伪代码(仅供参考具体功能实现应业务所需)

# 令牌桶法,具体步骤:
# 请求来了就计算生成的令牌数,生成的速率有限制
# 如果生成的令牌太多,则丢弃令牌
# 有令牌的请求才能通过,否则拒绝
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条/s ,那么计数器就是 5,在一秒内你每发送一条消息就减少一个,当你发第 6 条的时候计时器就不够了,那么这条消息就被丢弃了。

这种实现有点类似最开始介绍的固定窗口法,只不过时间粒度再小一些

基于queue的漏桶

基于 queue 的实现起来比较复杂,但是原理却比较简单,它也存在一个计数器,这个计数器却不表示速率限制,而是表示 queue 的大小,这里就是当有消息要发送的时候看 queue 中是否还有位置,如果有,那么就将消息放进 queue 中,这个 queue 以 FIFO 的形式提供服务;如果 queue 没有位置了,消息将被抛弃。

在消息被放进 queue 之后,还需要维护一个定时器,这个定时器的周期就是我们设置的频率周期,例如我们设置得频率是 5条/s,那么定时器的周期就是 200ms,定时器每 200ms 去 queue 里获取一次消息,如果有消息,那么就发送出去,如果没有就轮空。

这种实现方式比较复杂(我暂时还没看懂 有时间再去了解)

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模块,就可以轻松对付大多数的限流场景了

博主留言:

此篇博客是我自己结合网上的其他博主加上自己的理解写的,图片是自己画的可能不太美观,但是

意思简单就是希望你能结合图片理解,有助你使用以及学习。网上关于最后的漏桶法的代码实现

只是实现了水流进入桶的部分,没有实现关键水桶中漏出的部分 ,也就是说跟令牌桶没有太大区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值