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

Python使用Redis实现数据缓存与过期策略

2022-01-221.4k 阅读

1. Redis 简介

Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets),这使得它非常灵活且适用于各种场景。

1.1 Redis 优势

  1. 性能极高:Redis 是基于内存操作的,读写速度极快,能够轻松处理每秒数万次的读写操作,这对于需要快速响应的缓存场景非常关键。
  2. 数据结构丰富:除了常见的字符串类型,还支持哈希、列表等复杂数据结构。例如,在处理用户信息时,可以使用哈希结构来存储用户的多个属性。
  3. 支持持久化:Redis 提供了两种持久化方式,RDB(Redis Database)和 AOF(Append - Only File)。RDB 是将数据以快照的形式保存到磁盘,适合数据恢复;AOF 则是将写操作追加到日志文件,保证数据的完整性。
  4. 集群支持:Redis 从 3.0 版本开始支持集群模式,通过将数据分布在多个节点上,提高了系统的可扩展性和容错性。

2. Python 与 Redis 交互

在 Python 中,有多个库可以与 Redis 进行交互,其中最常用的是 redis - py 库。

2.1 安装 redis - py

可以使用 pip 进行安装:

pip install redis

2.2 连接 Redis

在 Python 代码中,连接 Redis 非常简单:

import redis

# 创建 Redis 连接
r = redis.Redis(host='localhost', port=6379, db = 0)

上述代码创建了一个到本地 Redis 服务器的连接,端口为 6379,使用的数据库编号为 0。Redis 默认有 16 个数据库,编号从 0 到 15。

3. 使用 Redis 实现数据缓存

缓存是 Redis 最常见的应用场景之一,它可以显著提高应用程序的性能,减少数据库等后端存储的负载。

3.1 简单缓存示例

假设我们有一个函数 get_user_info,它从数据库中获取用户信息。为了提高性能,我们可以将获取到的用户信息缓存到 Redis 中:

import redis


def get_user_info(user_id):
    r = redis.Redis(host='localhost', port=6379, db = 0)
    cache_key = f"user:{user_id}"
    cached_data = r.get(cache_key)
    if cached_data:
        return cached_data.decode('utf - 8')

    # 如果缓存中没有,从数据库获取
    user_info = "从数据库获取到的用户信息"  # 实际应用中应替换为真实数据库查询
    r.set(cache_key, user_info)
    return user_info

在上述代码中,首先尝试从 Redis 中获取用户信息。如果缓存中存在,则直接返回;否则,从数据库获取并将其存入缓存。

3.2 缓存复杂数据结构

Redis 支持多种数据结构,对于复杂数据,如字典或列表,可以使用哈希(hash)结构来缓存。例如,假设我们要缓存用户的多个属性:

import redis


def get_user_properties(user_id):
    r = redis.Redis(host='localhost', port=6379, db = 0)
    cache_key = f"user:properties:{user_id}"
    cached_data = r.hgetall(cache_key)
    if cached_data:
        result = {}
        for key, value in cached_data.items():
            result[key.decode('utf - 8')] = value.decode('utf - 8')
        return result

    # 如果缓存中没有,从数据库获取
    user_properties = {
        "name": "张三",
        "age": "25",
        "email": "zhangsan@example.com"
    }  # 实际应用中应替换为真实数据库查询
    pipe = r.pipeline()
    for key, value in user_properties.items():
        pipe.hset(cache_key, key, value)
    pipe.execute()
    return user_properties

这里使用 Redis 的哈希结构 hsethgetall 方法来缓存和获取用户的多个属性。通过 pipeline 可以批量执行命令,提高效率。

4. Redis 过期策略

Redis 支持为键设置过期时间,这在缓存场景中非常有用,可以确保缓存的数据不会永久占用内存,并且能够及时更新。

4.1 设置键的过期时间

redis - py 中,可以使用 setex 方法在设置键值对的同时设置过期时间(单位为秒):

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
r.setex("temp_key", 3600, "临时数据")  # 设置键 temp_key,过期时间为 3600 秒(1 小时)

也可以使用 set 方法先设置键值对,然后使用 expire 方法单独设置过期时间:

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
r.set("temp_key", "临时数据")
r.expire("temp_key", 3600)  # 设置键 temp_key 的过期时间为 3600 秒

4.2 过期策略原理

Redis 采用的过期策略是定期删除 + 惰性删除。

  1. 定期删除:Redis 会定期随机抽取一定数量的键进行检查,如果发现其中有过期的键,则将其删除。这个定期操作的频率是可以配置的,通过 hz 参数控制,默认 hz 为 10,即每秒执行 10 次过期检查。
  2. 惰性删除:当客户端尝试访问一个键时,如果该键已经过期,Redis 会在此时删除该键,并返回 nil。这种方式可以避免在过期键很多时,定期删除带来的性能开销。

然而,这两种策略也有一定的局限性。定期删除不能保证所有过期键都能及时删除,而惰性删除可能导致过期键在一段时间内仍然占用内存。为了解决这个问题,Redis 还提供了内存淘汰策略。

5. Redis 内存淘汰策略

当 Redis 内存达到设定的最大内存限制时,需要采用内存淘汰策略来决定删除哪些数据,以保证新数据的插入。

5.1 常见内存淘汰策略

  1. noeviction:默认策略,当内存不足时,新的写入操作会报错,不会淘汰任何数据。这种策略适用于不希望数据丢失的场景,但可能导致应用程序写入失败。
  2. volatile - lru:从设置了过期时间的键中,使用 LRU(Least Recently Used,最近最少使用)算法淘汰最近最少使用的键。
  3. allkeys - lru:从所有键中,使用 LRU 算法淘汰最近最少使用的键。这种策略适合缓存场景,因为它会优先淘汰长时间未被访问的键。
  4. volatile - ttl:从设置了过期时间的键中,淘汰剩余过期时间最短的键。
  5. allkeys - random:从所有键中随机淘汰键。
  6. volatile - random:从设置了过期时间的键中随机淘汰键。

5.2 设置内存淘汰策略

可以通过修改 Redis 配置文件(redis.conf)来设置内存淘汰策略:

maxmemory - policy allkeys - lru

也可以在运行时通过 CONFIG SET 命令来动态设置:

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
r.config_set('maxmemory - policy', 'allkeys - lru')

6. 结合数据缓存与过期策略的应用场景

6.1 网站页面缓存

在 Web 开发中,可以将整个页面或部分页面片段缓存到 Redis 中,并设置适当的过期时间。例如,对于一些不经常更新的新闻页面,可以缓存 1 小时,在这 1 小时内,用户访问该页面时直接从 Redis 中获取缓存内容,减少数据库查询和页面渲染时间。

import redis


def get_news_page(news_id):
    r = redis.Redis(host='localhost', port=6379, db = 0)
    cache_key = f"news:page:{news_id}"
    cached_page = r.get(cache_key)
    if cached_page:
        return cached_page.decode('utf - 8')

    # 如果缓存中没有,生成页面内容
    news_page_content = "生成的新闻页面内容"  # 实际应用中应替换为真实页面生成逻辑
    r.setex(cache_key, 3600, news_page_content)  # 缓存 1 小时
    return news_page_content

6.2 分布式系统中的缓存

在分布式系统中,多个服务可能需要共享一些数据。通过 Redis 作为缓存,可以实现数据的共享,并利用过期策略保证数据的一致性。例如,在一个电商系统中,多个服务可能需要获取商品的库存信息。可以将库存信息缓存到 Redis 中,并设置较短的过期时间,以确保库存信息的及时更新。

import redis


def get_product_stock(product_id):
    r = redis.Redis(host='localhost', port=6379, db = 0)
    cache_key = f"product:stock:{product_id}"
    cached_stock = r.get(cache_key)
    if cached_stock:
        return int(cached_stock.decode('utf - 8'))

    # 如果缓存中没有,从数据库获取
    product_stock = 100  # 实际应用中应替换为真实数据库查询
    r.setex(cache_key, 60, product_stock)  # 缓存 60 秒
    return product_stock

7. 注意事项与优化

7.1 缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,每次都会查询数据库,从而导致数据库压力过大。解决方法有两种:

  1. 布隆过滤器:在查询数据前,先通过布隆过滤器判断数据是否存在。布隆过滤器是一种概率型数据结构,它可以快速判断一个元素是否在集合中,虽然存在一定的误判率,但可以大大减少无效查询。
  2. 缓存空值:当查询数据库发现数据不存在时,也将空值缓存起来,并设置较短的过期时间,这样下次查询同样的数据时,直接从缓存中获取空值,避免查询数据库。

7.2 缓存雪崩

缓存雪崩是指在某一时刻,大量的缓存同时过期,导致大量请求直接打到数据库,造成数据库压力过大甚至崩溃。解决方法如下:

  1. 随机过期时间:在设置缓存过期时间时,不要设置相同的过期时间,而是在一个合理的时间范围内设置随机的过期时间,避免大量缓存同时过期。
  2. 使用互斥锁:在缓存过期后,只允许一个请求去查询数据库并更新缓存,其他请求等待。这样可以防止大量请求同时查询数据库。

7.3 缓存击穿

缓存击穿是指一个热点数据过期时,大量请求同时访问该数据,导致大量请求直接打到数据库。解决方法有:

  1. 热点数据不过期:对于热点数据,可以不设置过期时间,而是通过其他机制(如数据变更时主动更新缓存)来保证数据的一致性。
  2. 使用互斥锁:与缓存雪崩中使用互斥锁的原理类似,在热点数据过期时,只允许一个请求去查询数据库并更新缓存。

7.4 优化 Redis 性能

  1. 合理使用数据结构:根据实际需求选择合适的 Redis 数据结构,避免不必要的内存浪费和性能损耗。例如,如果只需要存储简单的键值对,使用字符串结构即可;如果需要存储多个相关属性,使用哈希结构更合适。
  2. 批量操作:尽量使用 pipeline 进行批量操作,减少网络开销。例如,在批量设置多个键值对时,可以使用 pipeline 一次性发送所有命令,而不是逐个发送。
  3. 监控与调优:使用 Redis 提供的监控工具(如 redis - cli 中的 INFO 命令)来监控 Redis 的运行状态,包括内存使用、命令执行次数等。根据监控结果,调整 Redis 的配置参数,如 maxmemoryhz 等,以优化性能。

通过合理使用 Redis 的数据缓存和过期策略,并注意上述优化点和注意事项,可以在 Python 应用程序中构建高效、稳定的缓存机制,提高应用程序的性能和可扩展性。在实际应用中,还需要根据具体业务场景进行灵活调整和优化,以达到最佳效果。