MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis在计数器与限流器中的实现策略

2022-10-081.6k 阅读

Redis 基础概述

Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set),这些丰富的数据结构为其在不同场景下的应用提供了强大的支持。

Redis 的数据存储在内存中,这使得它具有极高的读写性能。同时,它也提供了持久化机制,如 RDB(Redis Database)和 AOF(Append - Only - File),可以将内存中的数据保存到磁盘,以便在重启后恢复数据。

Redis 数据结构

  1. 字符串(String)
    • 字符串是 Redis 最基本的数据结构。它可以存储任何类型的数据,包括二进制数据。例如,可以存储用户的 ID、密码等简单信息。
    • 在 Redis 中设置字符串值的命令是 SET key value,获取值的命令是 GET key。例如,在 Redis 客户端中执行 SET user:1 "John",然后执行 GET user:1,就可以获取到 "John"
  2. 哈希(Hash)
    • 哈希结构用于存储字段和值的映射。它非常适合存储对象,比如一个用户的详细信息,每个字段可以是用户的属性,如姓名、年龄、邮箱等。
    • 设置哈希字段值的命令是 HSET key field value,获取字段值的命令是 HGET key field。例如,HSET user:1 name "John" age 30 email "john@example.com",通过 HGET user:1 name 可以获取到 "John"
  3. 列表(List)
    • 列表是一个有序的字符串元素集合。可以在列表的两端进行插入和删除操作,常用于实现消息队列、日志记录等功能。
    • 在列表头部插入元素的命令是 LPUSH key value,在列表尾部插入元素的命令是 RPUSH key value。例如,LPUSH mylist "element1"RPUSH mylist "element2",此时 mylist 中的元素顺序为 "element2""element1"
  4. 集合(Set)
    • 集合是一个无序的、不重复的字符串元素集合。常用于实现去重功能,比如统计网站的独立访客等。
    • 添加元素到集合的命令是 SADD key member,检查元素是否在集合中的命令是 SISMEMBER key member。例如,SADD myset "element1"SISMEMBER myset "element1" 会返回 1(表示存在)。
  5. 有序集合(Sorted Set)
    • 有序集合与集合类似,但每个元素都关联一个分数(score),通过分数来对元素进行排序。常用于排行榜等场景,比如游戏玩家的得分排行榜。
    • 添加元素到有序集合的命令是 ZADD key score member,获取排名范围内元素的命令是 ZRANGE key start stop [WITHSCORES]。例如,ZADD leaderboard 100 "player1" 200 "player2"ZRANGE leaderboard 0 -1 WITHSCORES 可以获取所有玩家及其得分并按得分排序。

计数器实现策略

简单计数器

  1. 基于字符串的简单计数器
    • 在 Redis 中,我们可以利用字符串数据结构来实现一个简单的计数器。由于 Redis 的字符串类型支持原子的递增和递减操作,这使得实现计数器变得非常容易。
    • 例如,我们要统计某个页面的访问次数,可以使用以下代码(以 Python 的 Redis 客户端 redis - py 为例):
import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

# 增加访问次数
r.incr('page:visit_count')

# 获取当前访问次数
count = r.get('page:visit_count')
print(f"当前页面访问次数: {count.decode('utf - 8')}")
  • 在上述代码中,r.incr('page:visit_count') 命令会原子性地将 page:visit_count 键对应的值增加 1。如果该键不存在,会先将其初始化为 0 再增加。r.get('page:visit_count') 获取当前的访问次数。这里需要注意的是,从 Redis 获取的值是字节类型,在 Python 中需要使用 decode('utf - 8') 方法将其转换为字符串。
  1. 哈希结构实现多维度计数器
    • 当我们需要统计多个维度的数据时,哈希结构就派上用场了。比如,我们要统计不同用户对不同页面的访问次数。
    • 以下是使用 Python 和 Redis 实现的代码:
import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

user_id = 'user1'
page_id = 'page1'

# 增加用户对页面的访问次数
r.hincrby(f'user:{user_id}:page_visits', page_id, 1)

# 获取用户对页面的访问次数
count = r.hget(f'user:{user_id}:page_visits', page_id)
print(f"用户 {user_id} 对页面 {page_id} 的访问次数: {count.decode('utf - 8') if count else 0}")
  • 在这段代码中,r.hincrby(f'user:{user_id}:page_visits', page_id, 1) 命令会原子性地增加指定用户对指定页面的访问次数。哈希表的键是 user:{user_id}:page_visits,字段是页面 ID,值是访问次数。r.hget 用于获取指定用户对指定页面的访问次数。如果该字段不存在,hget 会返回 None,我们将其处理为 0。

带过期时间的计数器

  1. 基于字符串的带过期时间计数器
    • 有时候,我们希望计数器在一段时间后自动重置,比如统计每分钟的请求次数。Redis 支持为键设置过期时间,结合简单的字符串计数器,我们可以实现这一功能。
    • 以下是 Python 代码示例:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 键名
key ='requests_per_minute'

# 设置过期时间为 60 秒
expiry_time = 60

while True:
    # 增加请求次数
    r.incr(key)
    # 设置键的过期时间
    r.setex(key, expiry_time, r.get(key))

    # 获取当前请求次数
    count = r.get(key)
    print(f"当前每分钟请求次数: {count.decode('utf - 8') if count else 0}")

    time.sleep(1)
  • 在上述代码中,r.setex(key, expiry_time, r.get(key)) 命令会设置键 requests_per_minute 的值为当前值,并设置过期时间为 60 秒。每次增加请求次数后都更新过期时间,这样每分钟计数器就会自动重置。
  1. 使用有序集合实现滑动窗口计数器
    • 滑动窗口计数器可以更灵活地统计一段时间内的请求次数,特别是在需要更细粒度控制时间窗口的场景下。我们可以使用 Redis 的有序集合来实现滑动窗口计数器。
    • 以下是 Python 实现代码:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 有序集合键名
key ='requests_sliding_window'
# 窗口大小(秒)
window_size = 60

while True:
    current_time = time.time()
    # 添加当前时间戳到有序集合
    r.zadd(key, {current_time: current_time})

    # 移除窗口外的时间戳
    r.zremrangebyscore(key, 0, current_time - window_size)

    # 获取窗口内的请求次数
    count = r.zcard(key)
    print(f"当前滑动窗口内请求次数: {count}")

    time.sleep(1)
  • 在这段代码中,r.zadd(key, {current_time: current_time}) 将当前时间戳添加到有序集合 requests_sliding_window 中,分数和成员都是当前时间戳。r.zremrangebyscore(key, 0, current_time - window_size) 移除窗口开始时间(当前时间减去窗口大小)之前的所有时间戳。r.zcard(key) 获取当前窗口内的元素数量,即请求次数。通过这种方式,我们实现了一个滑动窗口计数器,窗口大小为 60 秒。

限流器实现策略

固定窗口限流

  1. 基于计数器的固定窗口限流
    • 固定窗口限流是一种简单的限流策略,它将时间划分为固定长度的窗口,在每个窗口内统计请求次数,当请求次数超过设定的阈值时,限制后续请求。结合前面实现的计数器,我们可以很容易地实现固定窗口限流。
    • 以下是使用 Python 和 Redis 实现的固定窗口限流代码:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 键名
key = 'fixed_window_limit'
# 窗口大小(秒)
window_size = 60
# 阈值
limit = 100

while True:
    # 增加请求次数
    r.incr(key)
    # 设置键的过期时间
    r.setex(key, window_size, r.get(key))

    # 获取当前请求次数
    count = r.get(key)
    if int(count.decode('utf - 8')) > limit:
        print("请求超过限制,限流!")
    else:
        print("请求正常处理")

    time.sleep(1)
  • 在上述代码中,我们在每个时间窗口内统计请求次数。每次请求时,使用 r.incr(key) 增加请求次数,并通过 r.setex(key, window_size, r.get(key)) 设置键的过期时间为窗口大小。然后获取当前请求次数,如果超过阈值 limit,则进行限流处理,提示请求超过限制。
  1. 实现原理分析
    • 固定窗口限流的原理基于简单的计数器。在每个固定的时间窗口内,计数器记录请求的数量。当窗口结束时,计数器重置(通过设置键的过期时间实现)。这种方法的优点是实现简单,易于理解和部署。然而,它存在一个明显的问题,即临界问题。例如,当窗口大小为 60 秒,阈值为 100 次请求时,如果在第一个窗口的最后一秒和第二个窗口的第一秒各有 100 次请求,那么在这两秒内就会有 200 次请求,超过了预期的每秒平均请求数,可能导致系统过载。

滑动窗口限流

  1. 基于有序集合的滑动窗口限流
    • 滑动窗口限流可以解决固定窗口限流的临界问题。它通过将时间窗口划分为多个更小的子窗口,随着时间的推移,窗口像滑动一样移动,从而更精确地统计请求次数。我们使用 Redis 的有序集合来实现滑动窗口限流。
    • 以下是 Python 实现代码:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 有序集合键名
key ='sliding_window_limit'
# 窗口大小(秒)
window_size = 60
# 阈值
limit = 100
# 子窗口数量
sub_window_count = 10

while True:
    current_time = time.time()
    sub_window_size = window_size / sub_window_count

    # 添加当前时间戳到有序集合
    r.zadd(key, {current_time: current_time})

    # 移除窗口外的时间戳
    r.zremrangebyscore(key, 0, current_time - window_size)

    # 获取窗口内的请求次数
    count = r.zcount(key, current_time - window_size, current_time)

    if count > limit:
        print("请求超过限制,限流!")
    else:
        print("请求正常处理")

    time.sleep(1)
  • 在这段代码中,我们将窗口 window_size 划分为 sub_window_count 个子窗口。每次请求时,将当前时间戳添加到有序集合 sliding_window_limit 中。通过 r.zremrangebyscore(key, 0, current_time - window_size) 移除窗口外的时间戳。r.zcount(key, current_time - window_size, current_time) 获取窗口内的请求次数,与阈值 limit 比较来决定是否限流。
  1. 滑动窗口限流优势
    • 滑动窗口限流相比固定窗口限流更加精确。它通过细分时间窗口,使得限流能够更平滑地进行。例如,当窗口大小为 60 秒,子窗口数量为 10 时,每 6 秒为一个子窗口。在计算请求次数时,是基于整个滑动窗口内的所有子窗口的请求数量之和。这样可以避免固定窗口限流中在窗口切换时可能出现的瞬间请求量过大的问题,更有效地保护系统资源,防止系统因突发请求而过载。

漏桶限流

  1. 基于 Redis 实现漏桶限流
    • 漏桶限流的原理是想象有一个固定容量的桶,请求像水一样流入桶中,而桶以固定的速率漏水(处理请求)。如果桶满了(请求过多),多余的请求就会被丢弃(限流)。我们可以使用 Redis 的字符串数据结构和定时任务来模拟漏桶限流。
    • 以下是 Python 实现代码:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 漏桶容量
bucket_capacity = 100
# 漏水速率(每秒处理的请求数)
leak_rate = 10
# 键名
key = 'leaky_bucket'

# 初始化漏桶中的水量
if not r.exists(key):
    r.set(key, 0)

while True:
    current_water = int(r.get(key).decode('utf - 8'))
    # 漏水操作
    new_water = max(0, current_water - leak_rate)
    r.set(key, new_water)

    # 模拟请求到达
    new_requests = 5
    current_water = int(r.get(key).decode('utf - 8'))
    if current_water + new_requests <= bucket_capacity:
        r.incrby(key, new_requests)
        print("请求正常处理")
    else:
        print("请求超过限制,限流!")

    time.sleep(1)
  • 在上述代码中,我们首先初始化漏桶的水量(通过 Redis 的字符串键值对)。在每个时间间隔内,模拟漏水操作,即减少桶中的水量。然后模拟新请求到达,如果加上新请求后桶中的水量不超过桶的容量,则正常处理请求并增加桶中的水量;否则,进行限流处理。
  1. 漏桶限流特性
    • 漏桶限流的优点在于它可以平滑地处理请求,不管请求是突发的还是均匀的,它都能以固定的速率处理请求。这对于一些对系统稳定性要求较高的场景非常适用,比如防止服务器因瞬间大量请求而崩溃。然而,漏桶限流的缺点是如果请求突发量过大,超过桶的容量,多余的请求会被立即丢弃,可能导致用户体验不佳。

令牌桶限流

  1. 基于 Redis 实现令牌桶限流
    • 令牌桶限流的原理是系统以固定的速率生成令牌放入桶中,请求到达时需要从桶中获取令牌,如果桶中有足够的令牌,则请求被处理,否则请求被限流。我们可以使用 Redis 的字符串数据结构来实现令牌桶限流。
    • 以下是 Python 实现代码:
import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 令牌桶容量
bucket_capacity = 100
# 令牌生成速率(每秒生成的令牌数)
token_rate = 10
# 键名
key = 'token_bucket'

# 初始化令牌桶中的令牌数
if not r.exists(key):
    r.set(key, bucket_capacity)

while True:
    # 生成令牌
    current_tokens = int(r.get(key).decode('utf - 8'))
    new_tokens = min(bucket_capacity, current_tokens + token_rate)
    r.set(key, new_tokens)

    # 模拟请求到达
    required_tokens = 5
    current_tokens = int(r.get(key).decode('utf - 8'))
    if current_tokens >= required_tokens:
        r.decrby(key, required_tokens)
        print("请求正常处理")
    else:
        print("请求超过限制,限流!")

    time.sleep(1)
  • 在这段代码中,我们首先初始化令牌桶中的令牌数。在每个时间间隔内,按照令牌生成速率生成令牌,并确保令牌数不超过桶的容量。当有请求到达时,如果桶中的令牌数足够,则减少相应数量的令牌并处理请求;否则,进行限流处理。
  1. 令牌桶限流优势
    • 令牌桶限流相比漏桶限流更加灵活。它允许一定程度的突发请求,只要桶中有足够的令牌,突发请求就可以被处理。这在一些允许短时间内有较高请求量的场景中非常有用,比如电商网站的促销活动期间,用户请求可能会突然增加,但只要令牌桶中有足够的令牌,就可以在一定程度上满足用户的请求,同时又能通过令牌生成速率来控制总体的请求量,保护系统资源。

通过以上对 Redis 在计数器与限流器中的实现策略的介绍,我们可以看到 Redis 凭借其丰富的数据结构和高性能,为实现各种计数器和限流器提供了强大的支持。在实际应用中,我们可以根据具体的业务需求选择合适的实现策略,以确保系统的稳定性和性能。