前言:
“限流”顾名思义就是限制流量,它也是保护高并发系统三个法宝之一(保护高并发三种方式:缓存、
降级、限流),我们都知道服务器的处理能力是有一定上限的,如果超出这个上限继续把请求放进来,
可能会发生很多不可预料的麻烦事的。通过限流可以在超出我们预先设置的请求量最大值就让剩余的
请求排队或者拒绝服务,这样就可以让系统在扛不住高并发的情况下做到"有损服务"而不是导致服务
器崩掉彻底无法服务。
举个例子:最近华为新手机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模块,就可以轻松对付大多数的限流场景了
博主留言:
此篇博客是我自己结合网上的其他博主加上自己的理解写的,图片是自己画的可能不太美观,但是
意思简单就是希望你能结合图片理解,有助你使用以及学习。网上关于最后的漏桶法的代码实现
只是实现了水流进入桶的部分,没有实现关键水桶中漏出的部分 ,也就是说跟令牌桶没有太大区别