缓存文本数据

因为Redis把数据储存在内存里面,并且提供了方便的键值对索引方式以及多样化的数据类型,所以使用Redis作为缓存是Redis最常见的用法之一:

  • 很多国内外的社交平台都会把核心的时间线/信息流和好友关系/社交关系储存在Redis里面,这种做法不仅能够加快用户的访问速度,并且系统访问数据的方式也会变得更加简单、直接;

  • 还有不少追求访问速度的视频网站,也会把最常访问的静态文件放到Redis里面,又或者把短时间内最火爆的视频文件储存在Redis里面,从而尽可能地减少用户观看视频时需要等待的载入时间。

本书将介绍多种使用Redis缓存数据和文件的方法,其中:

  • 本章将介绍如何使用字符串键缓存单项数据(比如HTML页面),还有如何使用JSON和哈希键缓存多项数据(比如SQL表中的行)。

  • 下一章《缓存二进制数据》将会介绍缓存图片、视频文件等二进制数据的方法。

  • 至于缓存更复杂结构数据的方法(比如社交网站的时间线、好友关系等),则会在之后的章节中陆续介绍。

需求描述

你想要使用Redis缓存系统中的文本数据,这些数据可能只有单独一项,也可能会有多个项组成。

解决方案:使用字符串键缓存单项数据

有些时候,需要缓存的数据可能非常单纯,只有单独一项。比如说,在缓存Web服务器生成的HTML页面时,整个页面就是一个以<HTML>...</HTML>标签包围的字符串。在这种情况下,缓存程序只需要使用单个Redis字符串键就足以缓存整个页面。

具体来说,我们可以通过使用SET命令,将指定的名字和被缓存的内容关联起来:

SET name content

如果有需要的话,还可以在设置缓存的同时,为其设置过期时间以便让缓存实现自动更新:

SET name content EX ttl

至于获取缓存的工作则通过GET命令来完成:

GET name

代码清单 CODE_BASIC_CACHE 展示了基于这一原理实现的缓存程序。


代码清单 CODE_BASIC_CACHE 基本的缓存程序 cache.py

class Cache:

    def __init__(self, client):
        self.client = client

    def set(self, name, content, ttl=None):
        """
        为指定名字的缓存设置内容。
        可选的ttl参数用于设置缓存的生存时间。
        """
        if ttl is None:
            self.client.set(name, content)
        else:
            self.client.set(name, content, ex=ttl)

    def get(self, name):
        """
        尝试获取指定名字的缓存内容,若缓存不存在则返回None。
        """
        return self.client.get(name)

提示:提高过期时间精度

如果你需要更精确的过期时间,那么可以把缓存程序中过期时间的精度参数从代表秒的ex修改为代表毫秒的px

作为例子,代码清单 CODE_USAGE_BASIC_CACHE 展示了这个缓存程序的基本用法。


代码清单 CODE_USAGE_BASIC_CACHE 缓存程序的基本用例 cache_usage.py

from redis import Redis
from cache import Cache

ID = 10086
TTL = 60
REQUEST_TIMES = 5

client = Redis(decode_responses=True)
cache = Cache(client)

def get_content_from_db(id):
    # 模拟从数据库中取出数据
    return "Hello World!"

def get_post_from_template(id):
    # 模拟使用数据和模板生成HTML页面
    content = get_content_from_db(id)
    return "<html><p>{}</p></html>".format(content)

for _ in range(REQUEST_TIMES):
    # 尝试直接从缓存中取出HTML页面
    post = cache.get(ID)
    if post is None:
        # 缓存不存在,访问数据库并生成HTML页面
        # 然后把它放入缓存以便之后访问
        post = get_post_from_template(ID)
        cache.set(ID, post, TTL)
        print("Fetch post from database&template.")
    else:
        # 缓存存在,无需访问数据库也无需生成HTML页面
        print("Fetch post from cache.")

根据这段程序的执行结果可知,程序只会在第一次请求时访问数据库并根据模板生成HTML页面,而后续一分钟内发生的其他请求都是通过访问Redis保存的缓存来完成的:

$ python3 cache_usage.py
Fetch post from database&template.
Fetch post from cache.
Fetch post from cache.
Fetch post from cache.
Fetch post from cache.

解决方案:使用JSON/哈希键缓存多项数据

在复杂的系统中,单项数据往往只占少数,更多的是由多个项组成的复杂数据。比如表 TABLE_USERS 列出的这组用户信息,就来自SQL数据库Users表中的三个行,其中每个行由idname等四个属性值组成。


表 TABLE_USERS SQL数据库中的用户信息

id

name

gender

age

10086

"Peter"

"male"

56

10087

"Jack"

"male"

37

10088

"Mary"

"female"

24


可以通过两种不同的方法来缓存这类多项数据:

  1. 使用JSON等序列化手段将多项数据打包成单项数据,然后复用之前缓存单项数据的方法来缓存序列化数据。

  2. 使用Redis的哈希、列表等存储多项数据的数据结构来缓存数据。

接下来的两个小节将分别介绍这两种方法。

使用JSON缓存多项数据

代码清单 CODE_JSON_CACHE 展示了使用JSON缓存多项数据的方法。这个程序复用了之前代码清单 CODE_CACHE 的Cache类,它要做的就是在设置缓存之前把Python数据编码为JSON数据,并在获取缓存之后将JSON数据解码为Python数据。


代码清单 CODE_JSON_CACHE JSON版本的多项数据缓存程序 json_cache.py

import json
from cache import Cache

class JsonCache:

    def __init__(self, client):
        self.cache = Cache(client)

    def set(self, name, content, ttl=None):
        """
        为指定名字的缓存设置内容。
        可选的ttl参数用于设置缓存的生存时间。
        """
        json_data = json.dumps(content)
        self.cache.set(name, json_data, ttl)

    def get(self, name):
        """
        尝试获取指定名字的缓存内容,若缓存不存在则返回None。
        """
        json_data = self.cache.get(name)
        if json_data is not None:
            return json.loads(json_data)

作为例子,以下这段代码展示了如何使用这个程序来缓存前面展示的用户信息:

>>> from redis import Redis
>>> from json_cache import JsonCache
>>> client = Redis(decode_responses=True)
>>> cache = JsonCache(client)  # 创建缓存对象
>>> data = {"id":10086, "name": "Peter", "gender": "male", "age": 56}
>>> cache.set("User:10086", data)  # 缓存数据
>>> cache.get("User:10086")  # 获取缓存
{'id': 10086, 'name': 'Peter', 'gender': 'male', 'age': 56}

使用哈希缓存多项数据

除了将多项数据编码为JSON然后将其存储在字符串键里面,我们还可以直接将多项数据存储在Redis的哈希键中。为此,程序在设置缓存时需要用到HSET命令:

HSET name field value [field value] [...]

如果用户在设置缓存的同时还指定了缓存的生存时间,那么程序还需要使用EXPIRE命令为缓存哈希设置过期时间,并使用事务或者其他类似措施保证多个命令在执行时的安全性:

MULTI
HSET name field value [field value] [...]
EXPIRE name ttl
EXEC

与此相对,当程序需要获取被缓存的多项数据时,它只需要使用HGETALL命令获取所有数据即可:

HGETALL name

代码清单 CODE_HASH_CACHE 展示了基于上述原理实现的缓存程序。


代码清单 CODE_HASH_CACHE 使用哈希键实现的多项数据缓存程序 hash_cache.py

class HashCache:

    def __init__(self, client):
        self.client = client

    def set(self, name, content, ttl=None):
        """
        为指定名字的缓存设置内容。
        可选的ttl参数用于设置缓存的生存时间。
        """
        if ttl is None:
            self.client.hset(name, mapping=content)
        else:
            tx = self.client.pipeline()
            tx.hset(name, mapping=content)
            tx.expire(name, ttl)
            tx.execute()

    def get(self, name):
        """
        尝试获取指定名字的缓存内容,若缓存不存在则返回None。
        """
        result = self.client.hgetall(name)
        if result != {}:
            return result

作为例子,以下这段代码展示了如何使用这个缓存程序来缓存前面展示的用户信息:

>>> from redis import Redis
>>> from hash_cache import HashCache
>>> client = Redis(decode_responses=True)
>>> cache = HashCache(client)
>>> data = {"id":10086, "name": "Peter", "gender": "male", "age": 56}
>>> cache.set("User:10086", data)  # 缓存数据
>>> cache.get("User:10086")  # 获取缓存
{'id': '10086', 'name': 'Peter', 'gender': 'male', 'age': '56'}

可以看到,这个程序的效果跟之前使用JSON实现的缓存程序的效果完全一致。

提示:缩短键名以节约内存

在使用Redis缓存多项数据的时候,不仅需要缓存数据本身(值),还需要缓存数据的属性/字段(键)。当数据的数量非常巨大时,存储属性带来的内存消耗也会相当巨大,甚至不遑多让。

为此,缓存程序可以通过适当缩短属性名来尽可能地减少内存消耗。比如把上面用户信息中的name属性缩短为n属性,age属性缩短为a属性,诸如此类。

还有一种更彻底的方法,就是移除数据的所有属性,将数据本身存储为数组,然后根据各个值在数组中的索引来判断它们对应的属性。比如说,我们可以修改缓存程序,让它把数据{"id":10086, "name": "Peter", "gender": "male", "age": 56}简化为[10086, "Peter", "male", 56],然后使用JSON数组或是Redis列表来存储简化后的数据。

重点回顾

  • 因为Redis把数据储存在内存里面,并且提供了方便的键值对索引方式以及多样化的数据类型,所以使用Redis作为缓存是Redis最常见的用法之一。

  • 有些时候,需要缓存的数据可能非常单纯,只有单独一项。在这种情况下,缓存程序只需要使用单个Redis字符串键就足以缓存它们。

  • 在复杂的系统中,单项数据往往只占少数,更多的是由多个项组成的复杂数据。这时缓存程序可以考虑使用JSON等序列化手段,将多项数据打包为单项数据进行缓存,又或者直接使用Redis的哈希、列表等数据结构进行缓存。