书名:Redis应用实例
ISBN:978-7-115-65395-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 黄健宏
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书将从内部组件、外部应用和数据结构3个方面为读者介绍Redis常见、经典的用法与实例,并且所有实例均附有完整的Python代码,方便读者学习和参考。全书分3个部分:第一部分讲内部组件,介绍的实例通常用于系统内部,如缓存、锁、计数器、迭代器、速率限制器等,这些都是很多系统中不可或缺的部分;第二部分讲外部应用,介绍的实例都是一些日常常见的、用户可以直接接触到的应用,如直播弹幕、社交关系、排行榜、分页、地理位置等;第三部分讲数据结构,介绍的实例是一些使用Redis实现的常见数据结构,如先进先出队列、栈、优先队列和矩阵等。本书希望通过展示常见的Redis应用实例来帮助读者了解使用Redis解决各类问题的方法,并加深读者对Redis各项命令及数据结构的认识,使读者真正成为能够使用Redis解决各类问题的Redis专家。
本书适合对Redis有基本了解且想要进一步掌握Redis及键值数据库具体应用的技术人群,是理想的Redis技术进阶读物。
近年来,随着Redis大热并成为内存数据库的事实标准,大量关于Redis的图书也随之涌现。
2023年,在决定创作并推出全新的Redis图书之际,我对市面上已有的Redis图书进行了一番调研,发现大多数Redis图书关注的是命令、运维、架构、源码分析等方面的内容,而对实际应用只是一笔带过,或者在介绍命令时做锦上添花之用,很少有图书愿意详细地介绍使用Redis的应用实例。
然而,对Redis的大量使用导致网上关于Redis使用方法的各种问题越来越频繁地出现:如何使用Redis实现锁?如何使用Redis实现消息队列?如何使用Redis表示好友关系?如何使用Redis存储地理位置数据?随便去哪一个Redis社区,都会看到大量类似的问题。
考虑到这一点,我决定编写本书。书中包含32个精挑细选的经典Redis应用实例,如缓存、锁、计数器、消息队列、自动补全、社交关系、排行榜、先进先出队列等,这些实例无一不是我们日常开发中经常会遇到的,而且往往也是网上咨询最多的。
我希望通过在书中展示常见的Redis应用实例来帮助读者了解使用Redis解决各类问题的方法,并加深读者对Redis各项命令及数据结构的认识,使读者真正成为能够使用Redis解决各类问题的Redis专家。
本书通过大量Redis应用实例来展示Redis的经典用法与用例,全书分为3个部分,共32章。
第一部分讲内部组件。这个部分介绍的实例通常用于系统内部,如缓存、锁、计数器、迭代器、速率限制器等,这些都是很多系统中不可或缺的部分。通过学习如何使用Redis构建这些组件,并使用它们代替系统原有的低效组件,读者将能够大幅地提升系统的整体性能。
第二部分讲外部应用。这个部分介绍的实例都是一些日常常见的、用户可以直接接触到的应用,如直播弹幕、社交关系、排行榜、分页、地理位置等。通过学习如何使用Redis构建这些应用,读者将能够进一步地了解到Redis各个数据结构和命令的强大之处,还能够在实例应用已有功能的基础上,按需扩展出自己想要的其他功能。
第三部分讲数据结构。这个部分介绍的实例是一些使用Redis实现的常见数据结构,如先进先出队列、栈、优先队列和矩阵等。在需要快速、可靠的内存存储数据结构时,这些数据结构可以作为其他程序的底层数据结构或者基本构件使用。
除少数章引用了其他章的代码或内容之外,本书的大部分章都自成一体、可以独立阅读,读者可以按需阅读自己感兴趣的任意章。
当然,如果读者只是想要学习Redis的多种使用方法,并无特别喜好,也可以像阅读普通教程一样,按顺序阅读本书的每一章。本书基于难度和内容详略等因素对各章的顺序做了编排和优化,力求为读者带来流畅的阅读体验。
针对书中有难度的知识点,本书还配套提供了视频讲解“Redis应用十讲”,读者可以直接扫描第一次出现相关主题的对应章的二维码免费观看。视频讲解的具体内容如下:
● 第1讲“使用缓存提高访问数据的速度”(对应第1章和第2章);
● 第2讲“使用锁保证重要资源的独占使用权”(对应第3章和第4章);
● 第3讲“使用先进先出队列解决抢购和秒杀问题”(对应第26章和第27章);
● 第4讲“使用简单计数器和唯一计数器进行计数”(对应第6章和第7章);
● 第5讲“使用排行榜对元素进行有序排列”(对应第22章);
● 第6讲“使用自动补全为用户提供输入建议”(对应第16章);
● 第7讲“构建类似Stack Overflow等网站的投票系统”(对应第21章、第7章和第9章);
● 第8讲“使用分页和时间线排列并管理大量元素”(对应第23章和第24章);
● 第9讲“使用社交关系程序存储用户间的社交关系”(对应第18章);
● 第10讲“使用地理位置程序记录用户的位置信息”(对应第25章)。
阅读本书需要读者对Redis有一定的了解,并且熟悉Redis各个命令的基本语法。
因为本书关注的是如何使用Redis命令实现各种应用,而不是详细介绍某个或某些Redis命令的具体语法,所以刚开始学习Redis或者对Redis命令的语法并不熟悉的读者需要在阅读本书的过程中自行查找并学习书中提到的命令。相信这种边做边学、学以致用的方式将有助于读者快速、有效地掌握Redis命令及其用法,从而成为熟练的Redis使用者。
书中所有实例程序均使用Python编程语言编写,程序的风格以简单易懂为第一要务,基本上没有用到Python的高级特性。任何学过Python编程语言的读者都应该能很好地理解书中的代码,而没有学过Python编程语言的读者可以把这些朴素的代码看作伪代码,以此来理解程序想要完成的工作。
本书适合任何想要学习Redis应用构建方法和使用Redis解决实际问题的人,也可以作为Redis学习者在具备一定基础知识之后的进阶应用教程。
正如前文所言,本书展示的程序以简单易懂为第一要务,为了达到这个目的,本书有时候可能会故意把代码写得详细一些。
例如,为了清晰地展现判断语句的判断条件,本书将采用下面这样的具体写法:
if bool_value is True:
pass
而不是采用下面的简单写法:
if bool_value:
pass
又如,为了让没学过Python语言的人也能看懂程序的打开文件操作,本书将采用以下语句:
f = open(file, mode)
# do something
f.close()
而不是Python程序员更常用的with
语句:
with open(file, mode) as f:
# do something
基于上述原因,本书的部分代码对熟练的Python使用者来说可能会稍显啰唆,但这是事出有因的,希望读者可以理解。
本书在展示Python示例代码的时候,将使用标准的#
符号来标识Python代码中的注释:
>>> from random import random # 导入随机数生成函数
>>>
另外,由于本书在展示Redis操作时需要用到Redis官方客户端redis-cli,但该客户端并不支持注释语法,因此本书将采用自选的--
符号作为注释:
redis> PING -- 向服务器发送一个请求
PONG
因为redis-cli实际上并不支持这种注释语法,所以读者在把本书展示的Redis操作代码复制到redis-cli中运行时,请不要复制代码中的注释内容,以免代码在运行时出错。
本书聚焦实战,书中展示的各种实例无一不来源于实际的编程问题,但考虑到现实中的程序往往包含大量无关的逻辑和细节,在书中事无巨细地展示它们除模糊焦点和浪费篇幅之外,不会有其他任何好处。
举个例子,一个现实中的消息队列程序可能由数千行代码和数十个API组成,但如果仅在讲消息队列的第14章中就包含如此大量的代码和API,那么本书的篇幅将膨胀至让人无法接受的程度。
为了解决这个问题,本书采取了算法书介绍算法时的策略:不罗列和介绍每种应用可能包含的全部API,而是精挑细选出一组关键、核心的API,然后用简洁精练的代码在书中实现它们,配上合理的描述和解释,力求让读者尽可能地理解这些核心API的实现原理。一旦读者弄懂了这些核心API,就可以根据自己的需求移植这些应用,并在此基础上举一反三,为应用扩展出自己想要的任何API。
本书展示的所有Redis代码均在Redis 7.4版本中测试,Python代码均在Python 3.12版本中测试,使用的redis-py客户端版本为5.1.0b7,这是截至本书写作完成时这几种软件的最新版本。
要运行本书展示的代码和程序,读者需要在计算机上安装以上3种软件,并确保它们的版本不低于上面提到的版本。具体的软件安装方法请参考它们各自的官方网站。
本书展示的所有程序的源码都可以在异步社区(www.epubit.com)通过搜索本书书名找到下载链接,读者也可以通过执行以下命令克隆程序源码:
git clone git@github.com:huangzworks/rediscookbook.git
感谢人民邮电出版社杨海玲编辑在本书创作过程中的专业指导,感谢我的家人的悉心关照,还要感谢关注本书的读者对本书的期待,本书是在众多人的关心和支持下才得以完成的。
黄健宏
2024年9月于广东清远
内部组件部分介绍的实例通常用于系统内部,如缓存、锁、计数器、迭代器、速率限制器等,这些都是很多系统中不可或缺的部分。通过学习如何使用Redis构建这些组件,并用其代替系统原有的低效组件,读者将能够大幅地提升系统的整体性能。
因为Redis把数据存储在内存中,并且提供了方便的键值对索引方式以及多样化的数据类型,所以使用Redis作为缓存是Redis最常见的用法。
很多国内外的社交平台都会把核心的时间线/信息流和好友关系/社交关系存储在Redis中,这种做法不仅能够加快用户的访问速度,而且系统访问数据的方式也会变得更加简单、直接。不少追求访问速度的视频网站也会把经常访问的静态文件放到Redis中,或者把短时间内最火爆的视频文件存储在Redis中,从而尽可能地减少用户观看视频时需要等待的载入时间。
本书将介绍多种使用Redis缓存数据和文件的方法,其中本章将介绍如何使用字符串键缓存单项数据(如HTML文件的内容),还有如何使用JSON和哈希键缓存多项数据(如SQL表中的行);第2章将介绍缓存图片、视频文件等二进制数据的方法;至于缓存结构更复杂数据的方法(如社交网站的时间线、好友关系等),则会在之后的章节中陆续介绍。
使用Redis缓存系统中的文本数据。这些数据可能只有单独一项,也可能会由多个项组成。
有些时候,需要缓存的数据可能非常单纯,只有单独一项。例如,在缓存Web服务器生成的HTML页面时,整个页面就是一个以<HTML>...</HTML>
标签包围的字符串。在这种情况下,缓存程序只需要使用单个Redis字符串键就足以缓存整个页面。
具体来说,可以使用SET
命令,将指定的名字和被缓存的内容关联起来:
SET name content
如果需要,还可以在设置缓存的同时,为其设置过期时间以便让缓存实现自动更新:
SET name content EX ttl
至于获取缓存内容的工作则通过GET
命令来完成:
GET name
代码清单1-1展示了基于1.2节所述解决方案实现的缓存程序。
代码清单1-1 基本的缓存程序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
。
作为例子,下面这段代码展示了这个缓存程序的基本用法。
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.
在复杂的系统中,单项数据往往只占少数,更多的是由多个项组成的复杂数据。例如,表1-1列出的这组用户信息,就来自SQL数据库Users
表中的3行,每行由id
、name
、gender
和age
4个属性值组成。
表1-1 SQL数据库中的用户信息
id |
name |
gender |
age |
---|---|---|---|
10086 |
Peter |
male |
56 |
10087 |
Jack |
male |
37 |
10088 |
Mary |
female |
24 |
可以通过下面两种不同的方法来缓存这类多项数据。
● 使用JSON等序列化手段将多项数据打包成单项数据,然后复用之前缓存单项数据的方法来缓存序列化数据。
● 使用Redis的哈希、列表等存储多项数据的数据结构来缓存数据。
接下来介绍这两种方法。
代码清单1-2展示了使用JSON缓存多项数据的方法。这个程序复用了代码清单1-1 中的Cache
类,它要做的就是在设置缓存之前把Python数据编码为JSON数据,并在获取缓存之后将JSON数据解码为Python数据。
代码清单1-2 使用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
代码清单1-3展示了基于上述原理实现的多项数据缓存程序。
代码清单1-3 使用哈希键实现的多项数据缓存程序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的哈希、列表等数据结构进行缓存。