Redis-漏斗限流
漏斗限流
基本思路
漏斗限流是最常用的限流方法之一, 顾名思义, 这个算法的灵感来源于漏斗的结构。
漏斗的容量是优先的, 如果将漏斗嘴堵住, 然后一直往里面灌水, 它就会变满, 直至再也装不进去。如果将漏斗嘴放开, 水就会往下流, 水流走一部分后, 就又可以继续往里面灌水。 如果漏斗的流水速率大于灌水的速率, 那么漏斗永远都装不满。如果漏斗流水速率小于灌水速率, 那么一旦漏斗满了, 灌水就需要暂停并等待漏斗腾空。
所以漏斗的剩余空间就代表这当前行为可以持续进行的数量, 漏斗的流水速率代表着系统允许该行为的最大频率。 下面使用代码来描述单机漏斗算法:
class Funnel(object):
def __init__(self, capacity, leaking_rate):
# 漏斗容量
self.capacity = capacity
# 漏斗流水速率
self.leaking_rage = leaking_rate
# 漏斗剩余空间
self.left_quota = capacity
# self.leaking_ts = time.time()
def make_space(self):
not_ts = time.time()
# 距离上一次漏斗漏水过去了多久
delta_ts = now_ts - self.leaking_ts
# 又可以腾出不少空间了
delta_quota = delta_ts * self.leaking_rate
# 腾出的空间太少了, 那就等下次吧
if delta_quota < 1:
return
# 增加剩余空间
self.left_quota += delta_quota
# 记录漏水时间
self.leaking_ts = now_ts
# 剩余空间不能高于容量
if self.left_quota > self.capacity:
self.left_quota = self.capacity
def watering(self, quota):
self.make_space()
# 判断剩余空间是否足够
if self.left_quota >= quota:
self.left_quota -= quota
return True
return False
# 所有的漏斗
funnels = {}
def is_action_allowed(user_id, action_key, capacity, leaking_rate):
key = '%s:%s' %(user_id, action_key)
funnel = funnels.get(key)
if not funnel:
funnel = Funnel(capacity, leaking_rate)
funnels[key] = funnel
return funnel.watering(1)
for i in range(20):
print(is_allow_aoolwed('david', 'reply', 15, 0.5))
Funnel对象的make_space方法是漏斗算法的核心, 其在每次灌水前都会被调用以触发漏水, 给漏斗腾出空间来。能腾出多少空间取决于过去了多久以及流水的速率。 Funnel对象占据的空间大小不在和行为的频率成正比, 它的空间占用是一个常量。
问题来了, 分布式的漏斗算法该如何实现? 能不能使用Redis的基础数据结构来搞定?
我们观察Funnel对象的几个字段, 我们发现可以将Funnel对象的内容按字段存储到一个hash结构中, 灌水的时候将hash结构的字段取出来进行逻辑运算后, 再将新值回填到hash结构中就完成了一次行为频度的检测。
但是有个问题, 我们无法保证整个过程是原子性的。 从hash结构中取值, 然后在内存中运算, 在回填到hash结构, 这三个过程无法原子化, 意味着需要进行适当的加锁控制。而一旦加锁, 就意味着会有加锁失败,加锁失败就需要重新选择重试或者放弃。
如果重试的话, 就会导致性能下降。 如果放弃的话,就会影响用户体验。 同时, 代码的复杂度也跟着升高很多。 这是个艰难的选择, 该如何解决这个问题呢? Redis-Cell救星来了
Redis-Cell
Redis4.0提供了一个限流Redis模块, 它叫redis-cell。 该模块也使用了漏斗算法, 并提供了原子的限流指令。 有了这个模块, 限流问题就非常简单了。Redis-Cell安装步骤
该模块只有1条指令cl.throttle,它的参数和返回值都略显复杂, 接下来看看这个指令具体该如何使用。
127.0.0.1:6379>cl.throttle david:reply 15 30 60
# 0表示允许, 1表示拒绝
1) (integer) 0
# 漏斗容量capacity
2) (integer) 15
# 漏斗剩余空间left_quota
3) (integer) 14
# 如果拒绝了, 需要多长时间后再试(漏斗有空间了, 单位秒)
4) (integer) -1
# 多长时间后, 漏斗完全空出来(left_quota==capacity, 单位秒)
5) (integer) 2
在执行指令时, 如果被拒绝了, 就需要丢弃或重试。cl.throttle指令考虑的非常周到, 连重试时间都帮你算好了, 直接取返回结果数组的第四个值进行sleep即可, 不过不想阻塞线程, 也可以异步定时任务来重试。