Redis `INCR` 与 `DECR`:原子性数字操作与限流器实现

好的,各位程序猿朋友们,大家好!我是你们的老朋友,一个在代码堆里摸爬滚打多年的老司机。今天咱们不聊高大上的架构,也不谈深奥的算法,就聊聊 Redis 家族里两个朴实无华,但又威力无穷的小兄弟:INCRDECR

你可能会觉得,哎呀,这俩货谁不会啊?不就是加一减一嘛!But,事情可没那么简单。在并发的世界里,它们可是守护数据安全的钢铁侠,也是实现各种奇巧淫技的魔法师。

今天,我就要带大家深入挖掘 INCRDECR 的宝藏,看看它们是如何在原子性数字操作和限流器实现中大放异彩的。准备好了吗?Let’s go!

一、INCRDECR:Redis 世界里的“加减法”大师

首先,咱们先来认识一下这两位主角:

  • INCR key 将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行增一的操作。就像一个自动递增的计数器,每次调用,数字就往上蹦一格。

  • DECR key 将 key 中储存的数字值减一。同样,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行减一的操作。这是个倒计时器,滴答滴答,数字一点点变小。

这两个命令的返回值都是执行加/减操作后的值。

是不是很简单?但你可别小看它们,简单往往蕴含着强大的力量。

二、原子性:并发世界里的定海神针

在单线程的世界里,加一减一当然不在话下。但一旦涉及到并发,多个线程同时对一个数字进行操作,如果没有保护措施,数据就会乱成一锅粥。

想象一下,你在抢购一件限量版的商品,库存只有 1 件。两个线程同时读取到库存为 1,然后都执行了减 1 操作。如果没有原子性保证,最终的结果可能是库存变成了 -1,这就尴尬了!

Redis 的 INCRDECR 命令的精髓就在于它们的原子性。这意味着,无论有多少个客户端同时对同一个 key 执行 INCRDECR,Redis 都会保证这些操作是顺序执行的,不会发生任何的 race condition(竞争条件)。

你可以把 Redis 看作是一个非常敬业的“锁匠”,当一个客户端要对某个 key 进行加减操作时,Redis 会自动给这个 key 上一把锁,确保只有这个客户端可以操作。操作完成后,锁匠才会把锁打开,让其他客户端继续操作。

这种原子性保证,让 INCRDECR 在并发场景下成为了可靠的数据守护者。

三、INCRDECR 的应用场景:远不止加减那么简单

好了,知道了 INCRDECR 的基本概念和原子性,现在咱们来看看它们都能干些什么。

  1. 计数器:网站访问量、点赞数、浏览量

    这是 INCR 最经典的应用场景。每当有人访问你的网站,就调用 INCR page_view,访问量轻松搞定。点赞、浏览量同理。

    import redis
    
    # 连接 Redis
    r = redis.Redis(host='localhost', port=6379, db=0)
    
    # 增加页面访问量
    page_view = r.incr('page_view')
    print(f"当前页面访问量:{page_view}")
    
    # 增加点赞数
    like_count = r.incr('like_count')
    print(f"当前点赞数:{like_count}")
  2. 生成全局唯一 ID

    在分布式系统中,生成全局唯一的 ID 是一个常见的问题。INCR 可以轻松胜任这个任务。

    import redis
    
    # 连接 Redis
    r = redis.Redis(host='localhost', port=6379, db=0)
    
    # 生成全局唯一 ID
    order_id = r.incr('order_id')
    print(f"生成的订单 ID:{order_id}")

    每次调用 INCR order_id,都会生成一个新的、唯一的 ID。而且,由于 Redis 的高性能,这种方式生成 ID 的速度非常快。

  3. 限制用户行为:限流器

    这可是 INCRDECR 的重头戏!限流器是保护系统免受恶意攻击或流量过载的重要手段。INCRDECR 可以用来实现各种类型的限流器,比如:

    • 固定窗口计数器: 在一个固定的时间窗口内,限制用户的请求次数。
    • 滑动窗口计数器: 在一个滑动的时间窗口内,限制用户的请求次数。
    • 漏桶算法: 以恒定的速率处理请求,多余的请求放入桶中,如果桶满了就丢弃。
    • 令牌桶算法: 以恒定的速率向桶中放入令牌,用户请求需要获取令牌,如果桶中没有令牌就拒绝请求。

    咱们重点来看看固定窗口计数器和令牌桶算法的实现。

四、限流器实战:用 INCRDECR 打造流量卫士

  1. 固定窗口计数器

    固定窗口计数器的核心思想是:在一个固定的时间窗口内,统计用户的请求次数,如果超过了设定的阈值,就拒绝请求。

    import redis
    import time
    
    # 连接 Redis
    r = redis.Redis(host='localhost', port=6379, db=0)
    
    def fixed_window_rate_limit(user_id, limit, window_size):
        """
        固定窗口限流器
    
        Args:
            user_id: 用户 ID
            limit: 时间窗口内的最大请求次数
            window_size: 时间窗口大小(秒)
    
        Returns:
            True: 允许请求
            False: 拒绝请求
        """
        key = f"rate_limit:{user_id}"
        now = int(time.time())
    
        # 检查是否需要重置计数器
        if not r.exists(key):
            r.set(key, 0, ex=window_size) # 设置过期时间,自动重置
    
        # 增加计数器
        count = r.incr(key)
    
        # 判断是否超过阈值
        if count > limit:
            print(f"用户 {user_id} 超过限流阈值,拒绝请求")
            return False
        else:
            print(f"用户 {user_id} 允许请求,当前请求次数:{count}")
            return True
    
    # 示例
    user_id = "user123"
    limit = 5  # 限制 5 秒内只能请求 5 次
    window_size = 5
    
    for i in range(10):
        time.sleep(0.5)  # 模拟用户请求
        if fixed_window_rate_limit(user_id, limit, window_size):
            # 处理请求
            print("处理请求...")
        else:
            # 拒绝请求
            print("拒绝请求...")

    这个代码的核心逻辑是:

    • 使用 rate_limit:{user_id} 作为 Redis key,存储用户的请求次数。
    • 每次请求,先判断 key 是否存在,如果不存在,说明是新的时间窗口,需要初始化计数器,并设置过期时间为 window_size 秒。
    • 使用 INCR 命令增加计数器。
    • 判断计数器是否超过阈值 limit,如果超过,则拒绝请求。

    这种方式简单粗暴,但有一个明显的缺点:临界问题

    想象一下,如果用户在第一个时间窗口的最后一秒请求了 5 次,然后在第二个时间窗口的第一秒又请求了 5 次,那么用户在 2 秒内请求了 10 次,超过了限制。

  2. 令牌桶算法

    令牌桶算法是一种更复杂的限流算法,它可以平滑流量,避免突发流量对系统造成冲击。

    令牌桶算法的核心思想是:以恒定的速率向桶中放入令牌,用户请求需要获取令牌,如果桶中没有令牌就拒绝请求。

    import redis
    import time
    
    # 连接 Redis
    r = redis.Redis(host='localhost', port=6379, db=0)
    
    def token_bucket_rate_limit(user_id, capacity, rate):
        """
        令牌桶限流器
    
        Args:
            user_id: 用户 ID
            capacity: 令牌桶容量
            rate: 令牌生成速率(令牌/秒)
    
        Returns:
            True: 允许请求
            False: 拒绝请求
        """
        key = f"token_bucket:{user_id}"
        last_refill_time_key = f"token_bucket_refill_time:{user_id}"
    
        # 获取当前时间
        now = time.time()
    
        # 获取上次填充令牌的时间
        last_refill_time = r.get(last_refill_time_key)
        if last_refill_time is None:
            last_refill_time = now
            r.set(last_refill_time_key, now)
        else:
            last_refill_time = float(last_refill_time)
    
        # 计算应该填充的令牌数量
        refill_tokens = (now - last_refill_time) * rate
        refill_tokens = min(refill_tokens, capacity)  # 令牌数量不能超过桶的容量
    
        # 更新令牌数量
        current_tokens = r.get(key)
        if current_tokens is None:
            current_tokens = capacity
        else:
            current_tokens = float(current_tokens)
    
        current_tokens = min(current_tokens + refill_tokens, capacity)
    
        # 尝试获取令牌
        if current_tokens >= 1:
            new_tokens = current_tokens - 1
            r.set(key, new_tokens)
            r.set(last_refill_time_key, now)
            print(f"用户 {user_id} 允许请求,剩余令牌:{new_tokens}")
            return True
        else:
            print(f"用户 {user_id} 令牌不足,拒绝请求")
            return False
    
    # 示例
    user_id = "user123"
    capacity = 10  # 令牌桶容量为 10
    rate = 2  # 每秒生成 2 个令牌
    
    for i in range(15):
        time.sleep(0.2)  # 模拟用户请求
        if token_bucket_rate_limit(user_id, capacity, rate):
            # 处理请求
            print("处理请求...")
        else:
            # 拒绝请求
            print("拒绝请求...")

    这个代码的核心逻辑是:

    • 使用 token_bucket:{user_id} 作为 Redis key,存储令牌桶中的令牌数量。
    • 使用 token_bucket_refill_time:{user_id} 作为 Redis key,存储上次填充令牌的时间。
    • 每次请求,先计算应该填充的令牌数量,然后更新令牌桶中的令牌数量。
    • 如果令牌桶中的令牌数量大于等于 1,则允许请求,并从令牌桶中移除一个令牌。
    • 否则,拒绝请求。

    需要注意的是,这个代码中使用了 GETSET 命令,而不是 INCRDECR。这是因为我们需要计算填充的令牌数量,并保证令牌数量不超过桶的容量。

    当然,你也可以使用 Lua 脚本来实现令牌桶算法,这样可以保证整个过程的原子性。

五、总结:INCRDECR,小身材,大能量

好了,各位朋友们,今天咱们就聊到这里。

INCRDECR 就像 Redis 家族里的两颗螺丝钉,虽然不起眼,但却是构建各种复杂系统的基石。它们凭借着原子性的特性,在并发场景下保证了数据的安全,成为了计数器、全局唯一 ID 生成器和限流器的得力助手。

希望今天的分享能让你对 INCRDECR 有更深入的理解。记住,不要小看任何一个简单的工具,它们往往蕴含着巨大的潜力。

最后,送给大家一句话:代码的世界里,没有绝对的完美,只有不断地探索和优化。

感谢大家的收听,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注