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即可, 不过不想阻塞线程, 也可以异步定时任务来重试。

posted @ 2020-09-08 11:18  phper-liunian  阅读(354)  评论(0编辑  收藏  举报