【Redis基础和应用】(五)简单限流

本文介绍了如何使用Redis实现简单的限流策略,通过ZSet数据结构记录用户行为并限制频率。此外,探讨了漏斗限流算法,提出单机实现的Funnel类,并讨论了分布式环境下使用Redis基础数据结构实现漏斗限流的挑战。最后,提到了Redis 4.0的RedisCell模块,它提供了一种原子性的限流指令,简化了分布式限流的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单限流

0.前言

​ 限流算法在分布式领域中是一个经常被提起的话题,当系统处理的能力有限时,如何阻止计划外的请求继续对系统施压,是一个值得研究的问题。除了控制流量,限流还有一个应用,目的是控制用户行为,避免垃圾请求。比如在UGC(User Generated Content,用户原创内容)社区,用户发帖,回复,点赞行为都要严格受控,一般要严格限定某个行为在规定时间内被允许的次数。一旦超过次数,就需要采取一定的措施。比如禁止点击按钮,禁止发送请求,甚至禁号。

1. 使用方法

​ 一个常见的、简单的限流策略,系统要限定用户的某个行为在指定时间内只能允许发生N次,如何使用Redis的数据结构来实现这个限流功能?

#  先定义接口
def is_action_allowed(user_id: str, action_key: str, period: int, max_count: int) -> bool:
    """
        指定用户user_id的某个行为action_key在特定时间内period只允许发生的最多次数max_count
    """
    # TODO 限流
    return True


# 调用接口,一分钟只能只允许回复3个帖子
can_replay = is_action_allowed("leesure", "replay", 60, 3)
if can_replay:
    do_replay() # 回复帖子
else:
    raise ActionThresholdOverflow() # 异常

解决方案

​ 这个限流需求中存在一个定宽的滑动窗口,可以通过zset结构中的score值,圈出这个时间窗口,窗口之外的数据都可以去掉,zset的value存放的是毫秒级别的时间戳。如图所示,用一个zset结构记录用户的行为历史,每一个行为都会作为zset的item保存下来,同时这个item还包含该行为发生时的时间戳,同一个用户的同一种行为用一个zset记录。
在这里插入图片描述

​ 为了节省内存,只需要保存窗口内的行为记录。同时如果用户是冷用户,滑动时间窗口内的行为记录是空记录,那么这个zset就可以从内存中删除,不再占用空间。

​ 通过统计滑动窗口内的行为数量和max_count进行比较就可以得到当前的行为是否被允许。代码实现如下:

import time
import redis


class RedisDemo:
    def __init__(self):
        self.conn_pool = redis.ConnectionPool(host="localhost", port=6379, max_connections=8)
        self.client = redis.Redis(connection_pool=self.conn_pool, socket_timeout=3000, password="admin")

    def is_action_allowed(self, user_id, action_key, period, max_count):
        key = 'hist:%s:%s' % (user_id, action_key)
        now_ts = time.time()
        with self.client.pipeline() as pipe:
            pipe.zadd(key, {now_ts: now_ts})
            pipe.zremrangebyscore(key, 0, now_ts - period * 1000)
            pipe.zcard(key)
            pipe.expire(key, period + 1)
            _, _, current_count, _ = pipe.execute()
        return current_count <= max_count


if __name__ == '__main__':
    redis_client = RedisDemo()
    for i in range(10):
        can_replay = redis_client.is_action_allowed('leesure', 'replay', 60, 4)
        print(can_replay)
        time.sleep(1)

运行效果:

在这里插入图片描述

​ 这段代码的整体思路是:每一个行为到来时,都维护一次时间窗口,将时间窗口之外的记录全部清理掉,只保留窗口内的记录,zset中只有score值非常重要,value值没有特别的意义,只要保证它是唯一的就可以。

因为这几个连续的Redis操作都是针对一个Key的,使用pipeline可以显著提升Redis存储效率。

​ 但是这种方案也有缺点,因为他要记录时间窗口内的行为记录,如果这个量很大,比如60s内操作不得超过10万次。那么就不适合做这样的限流。

高级限流-漏斗限流

0. 前言

​ 漏斗限流是最常用的限流方法之一,这个算法的灵感来源于漏斗的结构。漏斗的容量是有限的,如果流出的速率大于流入的速率,那, 这个漏斗永远装不满,如果流出的速率小于流入的速率,那么漏斗一旦满了,就需要暂停流入,等待漏斗腾出一部分空间。

所以漏斗的剩余空间就代表:当前行为可以持续进行的数量,漏嘴的速率代表着系统允许该行为的最大速率。

1. 实现方法

单机漏斗算法:

import time


class Funnel:
    def __init__(self, capacity: int, leaking_rate: float):
        self.capacity = capacity  # 漏斗容量
        self.leaking_rate = leaking_rate  # 漏嘴流出速率
        self.left_quota = capacity  # 剩余空间
        self.pre_leaking_time = time.time()  # 上一次漏水时间

    def make_space(self):
        now_ts = time.time()
        d_ts = now_ts - self.pre_leaking_time
        d_quota = d_ts * self.leaking_rate
        if d_quota < 1:
            return
        # 增加剩余空间
        self.left_quota += d_quota
        # 记录漏水时间
        self.pre_leaking_time = now_ts
        if self.left_quota > self.capacity:
            self.left_quota = self.capacity

    def watering(self, quota) -> bool:
        self.make_space()
        if self.left_quota >= quota:
            self.left_quota -= quota
            return True
        return False


funnels = {}


# leaking_rate : quota / s
def is_action_allowed(user_id: str, action_key: str, capacity: int, leaking_rate: float):
    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)


if __name__ == '__main__':
    for i in range(10):
        can_do = is_action_allowed('leesure', 'replay', 3, 0.01)
        time.sleep(1)
        print(can_do)

​ Funnel对象的make_space()方法是漏斗算法的核心,在每次灌水前都会调用make_space()触发漏水,腾出空间,能腾出多少空间,取决于当前行为和上一次行为的时间差以及流出速度。而且Funnel对象占据的空间大小和行为频率无关,空间复杂度是O(1)

执行结果
在这里插入图片描述

✋问题来了,分布式的漏斗算法如何实现,能不能使用Redis的基础数据结构实现?

​ 观察Funnel对象的几个字段,发现可以将Funnel对象的内容按照字段存储到hash结构中,当有新的行为发生时,将hash结构字段取出来进行逻辑运算后,再将新的值回填到hash结构中就完成了一次行为频度检测

​ 但是有个问题,我们无法保证整个过程的原子性。从hash结构中取值,然后在内存中运算,再回填到hash结构中。这三个过程无法原子化。也就是说,需要进行适当的加锁控制,一旦加锁,就有加锁失败的可能,那么就需要选择重试或者放弃。如果重试,就会导致性能下降,如果放弃,就会影响用户体验,同时,代码复杂度也会提许多。

Redis Cell 就是为了解决上述问题的。

3. Redis Cell

​ Redis4.0提供了一个限流模块,Redis Cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有这个指令,限流问题就变得简单了。

​ 该模块只有一条指令cl.throttle。该指令的参数说明如下图所示.

在这里插入图片描述

​ 该指令的意思是:允许用户的“回复”行为的频率为每60s最多30次。漏斗的初始容量为15,也就是说,一开始可以连续回复15个帖子,然后才开始受漏斗速率的影响。这个指令中的 漏水速率变为了2个参数,代替了之前的单个浮点数。用两个参数相除的结果来表达漏水速率。

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

​ 在执行限流指令时,如果被拒绝了,就需要重试或者放弃,cl.throttle指令的返回值中包含了重试时间,直接获取第四个值,然后sleep(time)就可以了。如果不想阻塞线程,可以使用异步定时任务来重试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅宝儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值