书名:Redis入门指南(第2版)
ISBN:978-7-115-38840-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
李子骅(Luin),北京航空航天大学软件学院2009级本科生,阿里巴巴资深研发工程师。先后工作于街旁网、微软亚洲研究院和楽ニュー株式会社。乐于将自己对技术的感悟与他人分享。GitHub主页https://github.com/luin,博客http://zihua.li。
专业书评
作为国内第一本中文Redis图书,两年前出版的《Redis入门指南》第1版帮助了很多想要学习和了解Redis的读者。新版的《Redis入门指南》在旧版坚实的基础上进行了修正和更新,并增加了关于Redis 2.8 版本和3.0版本的新内容,使得本书更具阅读价值。无论是打算学习Redis的新手读者,还是想要了解Redis最新特性的Redis使用者,都不应该错过这本新版《Redis入门指南》。
——黄健宏,《Redis设计与实现》作者
作为一本Redis入门手册,这本书的介绍很全面,朴实的语言让工程师能很快上手,即便像知乎这样有不少Redis使用经验的团队,也能从中发现新鲜点,相信它对很多创业团队也会很有帮助。
——李申申,知乎网联合创始人、首席技术官
作为键值存储的Redis具有数据类型丰富和性能表现优异的特点。如果能够熟练地驾驭它,对很多大型应用都很有帮助。新浪作为世界上最大的Redis使用者,体验到了Redis为高并发在线业务带来的好处,但同时也遇到了很多挑战。作为国内第一本推进Redis普及的书,此书比较详细地介绍了Redis入门必备的基础知识,同时具有一些实践性方面的章节。如果你对Redis感兴趣,推荐你阅读此书,它会为你开启Redis的大门。
——杨海朝,新浪首席数据库架构师
在任何规模、任何类型的服务器项目中,都存在一些最适合用Redis存储的数据。而对Redis有了充分了解后,你就能把这个下一代的数据结构服务器用到最适合的地方。这本书可以帮助你成为Redis专家。
——刘昕,V2EX.com创始人
Redis作为可持久化的高性能键值存储服务,已经逐步成为各大互联网公司系统开发的首选。本书通过简单朴实的语言,深入浅出地介绍了Redis的各种使用方法和技巧,是一本不可多得的好书。
——吴一飞,腾讯公司高级软件工程师
最近几年Redis在国内的发展势头非常不错,很多公司开始选择Redis作为自己的缓存或小数据量存储方案,但目前市场上介绍Redis的相关书籍却非常匮乏。本书恰好弥补了这一缺口,是一本非常不错的入门和进阶书籍,书中介绍的应用实践案例也都是一些典型的应用场景,并在此之上深入介绍了一些Redis原理和优化的内容,相信读者读过之后会对Redis有一个非常全面而又深入的了解。
——田琪,腾讯公司高级工程师
与传统数据库相比,Redis提供了对多种数据结构的原生支持,在很多场合能够更方便地存储和处理数据。本书以各种实例带领读者走进Redis的世界,展现了Redis的独到之处,非常值得一读。
——刘其帅,豌豆荚后端工程师
本书卖点
★ 国内第一本中文Redis图书全新升级,涵盖Redis 3.0的全新内容
★ 多位业界专家联合推荐
★ 来自一线开发者的实战经验总结
★ 真正零基础入门,深入浅出全面剖析Redis
★ 任务驱动式学习,轻松掌握Redis实战知识
Redis是一个十分热门的内存数据库,它拥有诸多优良特性,已经被越来越多的公司采用,值得每一位Web开发者学习。
本书旨在帮助读者从零开始,一步步地了解Redis,进入Redis的世界。在介绍基础知识的同时,本书还着眼于实践,以帮助读者尽快在实际项目中用上Redis为目标,做到即学即用。书中大部分章节采用了任务驱动的讲解方法,通过虚构的角色小白和宋老师,在介绍知识点前先设定一个实践任务,不仅提高了读者的阅读动力,而且能让读者更轻易地将书中的知识点与实践结合起来。
主要内容
介绍Redis的历史及特性,回答为什么要使用Redis。
讲解如何在开发和生产环境中部署Redis。
通过实践看Redis诸多数据类型的适用场景。
介绍Redis 3.0的哨兵和集群相关特性。
全面介绍管道、事务、持久化和复制等技术。
深度揭秘Redis的存储结构,剖析Redis空间优化方法。
如何基于Redis实现在线用户记录、标签补全和区间查找等实用功能。
使用Redis实现任务队列。
领略Redis脚本的魅力,了解脚本的细节和技巧。
本书是一本Redis的入门指导书籍,以通俗易懂的方式介绍了Redis基础与实践方面的知识,包括历史与特性、在开发和生产环境中部署运行Redis、数据类型与命令、使用Redis实现队列、事务、复制、管道、持久化、优化Redis存储空间等内容,并采用任务驱动的方式介绍了PHP、Ruby、Python和Node.js这4种语言的Redis客户端库的使用方法。
本书的目标读者不仅包括Redis的新手,还包括那些已经掌握Redis使用方法的人。对于新手而言,本书的内容由浅入深且紧贴实践,旨在让读者真正能够即学即用;对于已经了解Redis的读者,通过本书的大量实例以及细节介绍,也能发现很多新的技巧。
Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年。随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问。而Redis的迅猛发展,为这个领域注入了全新的思维。
Redis凭借其全面的功能得到越来越多的公司的青睐,从初创企业到新浪微博这样拥有着几百台Redis服务器的大公司,都能看到Redis的身影。Redis也是一个名副其实的多面手,无论是存储、队列还是缓存系统,都有它的用武之地。
本书将从Redis的历史讲起,结合基础与实践,带领读者一步步进入Redis的世界。
在本书第1版截稿的时候,加入了Lua脚本功能的Redis 2.6版刚刚发布,此时的Redis正在逐渐地被国内的开发者所熟知。如今整整两年过去了,Redis也即将发布新的里程碑版本3.0版。在这两年中,Redis增加了许多优秀的功能,同时也被越来越多的公司所采用与信赖。在写这段文字时,恰好Redis的作者Salvatore Sanfilippo转述了别人的一句话:“如果把Redis官网的‘谁在使用Redis’页面改名为‘谁没在使用Redis’,那么这个页面的内容一定会精简不少。”虽然是一句玩笑话,但是也从侧面体现出这两年里Redis的飞速发展。而继续编写《Redis入门指南》第2版的最大动力也是希望将Redis发展的成果及时地与广大读者分享,同时也借此感谢大家对本书第1版的积极反馈。
本书假定读者是Redis的新手,甚至可能连Redis是什么都没听说过。本书将会详细地介绍Redis是什么以及为什么要使用Redis,旨在能让读者从零开始逐步晋升为一个优秀的Redis开发者。
本书还包含了很多Redis实践方面的知识,对于有经验的Redis开发者,大可以直接跳过已经掌握的内容,只阅读感兴趣的部分。每章的引言都简要介绍了本章要讲解的内容,供读者参考。
本书并不需要读者有任何Redis的背景知识,不过如果读者有Web后端开发经验或Linux系统使用经验,阅读本书将会更加得心应手。
第1章介绍了Redis的历史与特性,主要回答两个初学者最关心的问题,即Redis是什么和为什么要使用Redis。
第2章讲解了如何安装和运行Redis。如果你身旁的计算机没有运行Redis,那么一定不要错过这一章,因为本书后面的部分都需要读者最好能一边阅读一边实践,以提高学习效率。本章中还会介绍Redis命令行客户端的使用方法等基础知识,这些都是实践前需要掌握的知识。
第3章介绍了Redis的数据类型。本章讲解的不仅是每个数据类型的介绍和命令的格式,还会着重讲解每个数据类型分别在实践中如何使用。整个第3章会带领读者从零开始,一步步地使用Redis构建一个博客系统,旨在帮助读者在学习完本章的内容之后可以直接在自己的项目中上手实践Redis。
第4章引入了一些Redis的进阶知识,比如事务和消息系统等。同样本章还会继续以博客系统为例子,以实践驱动学习。
第5章介绍了如何在各个编程语言中使用Redis,这些语言包括PHP、Ruby、Python和Node.js。其中讲解每种语言时最后都会以一个有趣的例子作为演示,即使你不了解某些语言,阅读这些例子也能让你收获颇丰。
第6章展示了Redis脚本的强大功能。本章会向读者讲解如何借助脚本来扩展Redis,并且会对脚本一些需要注意的地方(如沙盒、随机结果等)进行着重介绍。
第7章会介绍Redis持久化的知识。Redis持久化包含RDB和AOF两种方式,对持久化的支持是Redis之所以可以用作数据库的必要条件。
第8章详细说明了多个Redis实例的维护方法,包括使用复制实现读写分离、借助哨兵来自动完成故障恢复以及通过集群来实现数据分片。
第9章介绍了Redis安全和协议相关的内容,并向会推荐几个第三方的Redis管理工具。
附录A收录了Redis命令的不同属性,以及属性的特征。
附录B收录了Redis部分配置参数的章节索引。
附录C收录了Redis使用的CRC16实现代码。
本书排版使用字体遵从以下约定。
$ redis-cli PING
PONG
redis> SET foo bar
OK
var redis = require("redis");
var client = redis.createClient();
// 将两个对象JSON序列化后存入数据库中
client.mset(
'user:1', JSON.stringify(bob),
'user:2', JSON.stringify(jeff)
);
本书的部分章节采用了伪代码来讲解,这种伪代码类似Ruby和PHP,例如:
def hsetnx($key, $field, $value)
$isExists = HEXISTS $key, $field
if $isExists is 0
HSET $key, $field, $value
return 1
else
return 0
其中变量使用$符号标识,Redis命令使用的粗体表示并省略了括号以便于阅读。在命令调用和print等语句中没有$符号的字符串会被当作字符串字面值。
本书第5章中每一节都包含了一个完整的程序,通常来讲,读者最好自己输入这些代码来加深理解,当然如果要先看到程序的运行结果再开始学习也不失为一个好办法。
这些程序代码都存放在GitHub上(https://github.com/luin/redis-book-assets),供读者查看和下载。读者也可以从异步社区(https://www.epubit.com)本书页面下载。
在本书写作的过程中,得到了很多朋友的帮助。请允许我在这里占用少许篇幅,向他们致以诚挚的谢意。
感谢人民邮电出版社的杨海玲老师对本书的支持,没有她的悉心指导,本书就无法顺利完成。
感谢刘亚晨、李欣越、寇祖阳和余尧,他们承担了许多额外的工作,使得我可以全身心地投入到写作中。
感谢所有浏览本书初稿并提出意见和建议的人们:张沈鹏、陈硕实、刘其帅、扈煊、李其超、朱冲宇、王诗吟、黄山月、刘昕、韩重远、李申申、杨海朝、田琪等。感谢你们的支持。
另外还要感谢“宋老师”,是的,就是书中的主人公之一。几年前,我刚创业时,办公场所是和某教育机构合租的,宋老师是该机构的一名英语老师,同时他也是国内一个知名的嘻哈乐团成员之一。他平日风趣的谈吐带给了我们很多欢乐,伴随我们走过了艰苦的创业初期,而我接触Redis,也正是从这段时间开始的。
最后,感谢我的父母和女朋友马梦妍,你们是我生命中最重要的人,感谢你们的理解和支持。
“纸上得来终觉浅,绝知此事要躬行。”
——陆游《冬夜读书示子聿》
学习Redis最好的办法就是动手尝试它。在介绍Redis最核心的内容之前,本章先来介绍一下如何安装和运行Redis,以及Redis的基础知识,使读者可以在之后的章节中一边学习一边实践。
安装Redis是开始Redis学习之旅的第一步。在安装Redis前需要了解Redis的版本规则以选择最适合自己的版本,Redis约定次版本号(即第一个小数点后的数字)为偶数的版本是稳定版(如2.8版、3.0版),奇数版本是非稳定版(如2.7版、2.9版),生产环境下一般需要使用稳定版本。本书的内容以3.0版为目标编写,同时绝大部分内容也适用于2.6版和2.8版。对于只在最新版才有的特性(如Cluster集群),本书会做特别说明。
Redis兼容大部分POSIX系统,包括Linux、OS X和BSD等,在这些系统中推荐直接下载Redis源代码编译安装以获得最新的稳定版本。Redis最新稳定版本的源代码可以从地址http://download.redis.io/redis-stable.tar.gz下载。
下载安装包后解压即可使用make命令
完成编译,完整的命令如下:
wget http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
Redis没有其他外部依赖,安装过程很简单。编译后在Redis源代码目录的src
文件夹中可以找到若干个可执行程序,最好在编译后直接执行make install
命令来将这些可执行程序复制到/usr/local/bin目录中以便以后执行程序时可以不用输入完整的路径。
在实际运行Redis前推荐使用make test
命令测试Redis是否编译正确,尤其是在编译一个不稳定版本的Redis时。
提示
除了手工编译外,还可以使用操作系统中的软件包管理器来安装Redis,但目前大多数软件包管理器中的Redis的版本都较古老。考虑到 Redis 的每次升级都提供了对以往版本的问题修复和性能提升,使用最新版本的 Redis 往往可以提供更加稳定的体验。如果希望享受包管理器带来的便利,在安装前请确认您使用的软件包管理器中Redis的版本并了解该版本与最新版之间的差异。http://redis.io/topics/problems中列举了一些在以往版本中存在的已知问题。
OS X下的软件包管理工具Homebrew和MacPorts均提供了较新版本的Redis包,所以我们可以直接使用它们来安装Redis,省去了像其他POSIX系统那样需要手动编译的麻烦。下面以使用Homebrew安装Redis为例。
在终端下输入ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
即可安装Homebrew。
如果之前安装过Homebrew,请执行brew update来
更新Homebrew,以便安装较新版的Redis。
使用brew install软件包名
可以安装相应的包,此处执行brew install redis
来安装Redis:
$brew install redis
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/redis-3.0.0.yosemite.bottle.tar.gz
######################################################################## 100.0%
==> Pouring redis-3.0.0.yosemite.bottle.tar.gz
==> Caveats
To have launchd start redis at login:
ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
Then to load redis now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
Or, if you don't want/need launchctl, you can just run:
redis-server /usr/local/etc/redis.conf
==> Summary
/usr/local/Cellar/redis/3.0.0: 10 files, 1.4M
OS X系统从Tiger版本开始引入了launchd工具来管理后台程序,如果想让Redis随系统自动运行可以通过以下命令配置launchd:
ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
通过launchd运行的Redis会加载位于/usr/local/etc/redis.conf的配置文件,关于配置文件会在2.4节中介绍。
Redis官方不支持Windows。2011年微软1向Redis提交了一个补丁,以使Redis可以在Windows下编译运行,但被Salvatore Sanfilippo拒绝了,原因是在服务器领域上Linux已经得到了广泛的使用,让Redis能在Windows下运行相比而言显得不那么重要。并且Redis使用了如写时复制等很多操作系统相关的特性,兼容Windows会耗费太大的精力而影响Redis其他功能的开发。尽管如此微软还是发布了一个可以在Windows运行的Redis分支2,而且更新相当频繁,截止到本书交稿时,Windows下的Redis版本为2.8。
1微软开放技术有限公司(Microsoft Open Technologies Inc.),专注于参与开源项目、开放标准工作组以及提出倡议。
2https://github.com/MSOpenTech/Redis
如果想使用Windows学习或测试Redis可以通过Cygwin软件或虚拟机(如VirtualBox)来完成。Cygwin能够在Windows中模拟Linux系统环境。Cygwin实现了一个Linux API接口,使得大部分Linux下的软件可以重新编译后在Windows下运行。Cygwin还提供了自己的软件包管理工具,让用户能够方便地安装和升级几千个软件包。借助Cygwin,我们可以在Windows上通过源代码编译安装最新版的Redis。
从Cygwin官方网站下载setup.exe程序,setup.exe既是Cygwin的安装包,又是Cygwin的软件包管理器。运行setup.exe后进入安装向导。前几步会要求选择下载源、安装路径、代理和下载镜像等,可以根据具体需求选择,一般来说一路点击“Next”即可。之后会出现软件包管理界面,如图2-1所示。
图2-1 Cygwin包管理界面
编译安装Redis需要用到的包有gcc和make,二者都可以在“Devel”分类中找到。在“New”字段中标记为“Skip”的包表示不安装,单击“Skip”切换成需要安装的版本号即可令Cygwin在稍后安装该版本的包。图2-1中所示gcc包的状态为“Keep”是因为作者之前已经安装过该包了,同样如果读者在退出安装向导后还想安装其他软件包,只需要重新运行setup.exe程序再次进入此界面即可。
为了方便使用,我们还可以安装wget(用于下载Redis源代码,也可以手动下载并使用Windows资源管理器将其复制到Cygwin对应的目录中,见下文介绍)和vim(用于修改Redis的源代码使之可以在Cygwin下正常编译)。
之后单击“Next”,安装向导就会自动完成下载和安装工作了。
安装成功后打开Cygwin Terminal程序即可进入Cygwin环境,Cygwin会将Windows中的目录映射到Cygwin中。如果安装时没有更改安装目录,Cygwin环境中的根目录对应的Windows中的目录是C:\cygwin。
下载和解压Redis的过程和2.1.1节中介绍的一样,不过在make
之前还需要修改Redis的源代码以使其可以在Cygwin下正常编译。
首先编辑src目录下的redis.h文件,在头部加入:
#ifdef CYGWIN
#ifndef SA ONSTACK
#define SA ONSTACK 0x08000000
#endif
#endif
而后编辑src目录下的object.c文件,在头部加入:
#define strtold(a,b) ((long double)strtod((a),(b)))
同2.1.1节一样,执行make
命令即可完成编译。
注意
Cygwin环境无法完全模拟Linux系统,比如Cygwin的fork不支持写时复制;另外,Redis官方也并不提供对Cygwin的支持,Cygwin环境只能用于学习Redis。运行Redis的最佳系统是Linux和OS X,官方推荐的生产系统是Linux。
安装完Redis后的下一步就是启动它,本节将分别介绍在开发环境和生产环境中运行Redis的方法以及正确停止Redis的步骤。
在这之前首先需要了解Redis包含的可执行文件都有哪些,表2-1中列出了这些程序的名称以及对应的说明。如果在编译后执行了make install命令,这些程序会被复制到/usr/local/bin目录内,所以在命令行中直接输入程序名称即可执行。
表2-1 Redis可执行文件说明
文 件 名 |
说 明 |
---|---|
redis-server |
Redis服务器 |
redis-cli |
Redis命令行客户端 |
redis-benchmark |
Redis性能测试工具 |
redis-check-aof |
AOF文件修复工具 |
redis-check-dump |
RDB文件检查工具 |
redis-sentinel |
Sentinel服务器(仅在2.8版以后) |
我们最常使用的两个程序是redis-server和redis-cli,其中redis-server是Redis的服务器,启动Redis即运行redis-server;而redis-cli是Redis自带的Redis命令行客户端,是学习Redis的重要工具,2.3节会详细介绍它。
启动Redis有直接启动和通过初始化脚本启动两种方式,分别适用于开发环境和生产环境。
直接运行redis-server即可启动Redis,十分简单:
$ redis-server
[5101] 14 Dec 20:58:59.944 # Warning: no config file specified, using the default config.
In order to specify a config file use redis-server /path/to/redis.conf
[5101] 14 Dec 20:58:59.948 * Max number of open files set to 10032
...
[5101] 14 Dec 20:58:59.949 # Server started, Redis version 2.6.9
[5101] 14 Dec 20:58:59.949 * The server is now ready to accept connections on port 6379
Redis服务器默认会使用6379端口3,通过--port
参数可以自定义端口号:
36379是手机键盘上MERZ对应的数字,MERZ是一名意大利歌女的名字。
$redis-server --port 6380
在Linux系统中可以通过初始化脚本启动Redis,使得Redis能随系统自动运行,在生产环境中推荐使用此方法运行Redis,这里以Ubuntu和Debian发行版为例进行介绍。在Redis源代码目录的utils文件夹中有一个名为redis_init_script的初始化脚本文件,内容如下:
#!/bin/sh
#
# Simple Redis init.d script conceived to work on Linux systems
# as it does use of the /proc filesystem.
REDISPORT=6379
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli
PIDFILE=/var/run/redis_${REDISPORT}.pid
CONF="/etc/redis/${REDISPORT}.conf"
case "$1" in
start)
if [ -f $PIDFILE ]
then
echo "$PIDFILE exists, process is already running or crashed"
else
echo "Starting Redis server..."
$EXEC $CONF
fi
;;
stop)
if [ ! -f $PIDFILE ]
then
echo "$PIDFILE does not exist, process is not running"
else
PID=$(cat $PIDFILE)
echo "Stopping ..."
$CLIEXEC -p $REDISPORT shutdown
while [ -x /proc/${PID} ]
do
echo "Waiting for Redis to shutdown ..."
sleep 1
done
echo "Redis stopped"
fi
;;
*)
echo "Please use start or stop as first argument"
;;
esac
我们需要配置Redis的运行方式和持久化文件、日志文件的存储位置等,具体步骤如下。
(1)配置初始化脚本。首先将初始化脚本复制到/etc/init.d目录中,文件名为redis_
端口号,其中端口号表示要让Redis监听的端口号,客户端通过该端口连接Redis。然后修改脚本第6行的REDISPORT
变量的值为同样的端口号。
(2)建立需要的文件夹。建立表2-2中列出的目录。
表2-2 需要建立的目录及说明
目 录 名 |
说 明 |
---|---|
|
存放Redis的配置文件 |
|
存放Redis的持久化文件 |
(3)修改配置文件。首先将配置文件模板(见2.4节介绍)复制到/etc/redis目录中,以端口号命名(如“6379.conf”),然后按照表2-3对其中的部分参数进行编辑。
表2-3 需要修改的配置及说明
参 数 |
值 |
说 明 |
---|---|---|
|
|
使Redis以守护进程模式运行 |
|
|
设置Redis的PID文件位置 |
|
|
设置Redis监听的端口号 |
|
|
设置持久化文件存放位置 |
现在就可以使用/etc/init.d/redis_
端口号start
来启动Redis了,而后需要执行下面的命令使Redis随系统自动启动:
$sudo update-rc.d redis_端口号 defaults
考虑到Redis有可能正在将内存中的数据同步到硬盘中,强行终止Redis进程可能会导致数据丢失。正确停止Redis的方式应该是向Redis发送SHUTDOWN
命令,方法为:
$redis-cli SHUTDOWN
当Redis收到SHUTDOWN
命令后,会先断开所有客户端连接,然后根据配置执行持久化,最后完成退出。
Redis可以妥善处理SIGTERM
信号,所以使用kill Redis
进程的PID
也可以正常结束Redis,效果与发送SHUTDOWN
命令一样。
还记得我们刚才编译出来的redis-cli程序吗?redis-cli(Redis Command Line Interface)是Redis自带的基于命令行的Redis客户端,也是我们学习和测试Redis的重要工具,本书后面会使用它来讲解Redis各种命令的用法。
本节将会介绍如何通过redis-cli向Redis发送命令,并且对Redis命令返回值的不同类型进行简单介绍。
通过redis-cli向Redis发送命令有两种方式,第一种方式是将命令作为redis-cli的参数执行,比如在2.2.2节中用过的redis-cli SHUTDOWN
。redis-cli执行时会自动按照默认配置(服务器地址为127.0.0.1,端口号为6379)连接Redis,通过-h
和-p
参数可以自定义地址和端口号:
$redis-cli -h 127.0.0.1 -p 6379
Redis提供了PING
命令来测试客户端与Redis的连接是否正常,如果连接正常会收到回复PONG
。如:
$redis-cli PING
PONG
第二种方式是不附带参数运行redis-cli,这样会进入交互模式,可以自由输入命令,例如:
$redis-cli
redis 127.0.0.1:6379>PING
PONG
redis 127.0.0.1:6379>ECHO hi
"hi"
这种方式在要输入多条命令时比较方便,也是本书中主要采用的方式。为了简便起见,后文中我们将用redis>
表示redis 127.0.0.1:6379>
。
在大多数情况下,执行一条命令后我们往往会关心命令的返回值,如1.2.4节中的HGET命令的返回值就是我们需要的指定键的title字段的值。命令的返回值有5种类型,对于每种类型redis-cli的展现结果都不同,下面分别说明。
状态回复(status reply)是最简单的一种回复,例如,向Redis发送SET
命令设置某个键的值时,Redis会回复状态OK
表示设置成功。另外之前演示的对PING
命令的回复PONG
也是状态回复。状态回复直接显示状态信息,如:
redis>PING
PONG
当出现命令不存在或命令格式有错误等情况时Redis会返回错误回复(error reply)。错误回复以(error)
开头,并在后面跟上错误信息。如执行一个不存在的命令:
redis>ERRORCOMMEND
(error) ERR unknown command 'ERRORCOMMEND'
在2.6版本时,错误信息均是以“ERR”开头,而在2.8版以后,部分错误信息会以具体的错误类型开头,如:
redis>LPUSH key 1
(integer) 1
redis>GET key
(error) WRONGTYPE Operation against a key holding the wrong kind of value
这里错误信息开头的“WRONGTYPE”就表示类型错误,这个改进使得在调试时能更容易地知道遇到的是哪种类型的错误。
Redis虽然没有整数类型,但是却提供了一些用于整数操作的命令,如递增键值的INCR
命令会以整数形式返回递增后的键值。除此之外,一些其他命令也会返回整数,如可以获取当前数据库中键的数量的DBSIZE
命令等。整数回复(integer reply)以(integer)
开头,并在后面跟上整数数据:
redis>INCR foo
(integer) 1
字符串回复(bulk reply)是最常见的一种回复类型,当请求一个字符串类型键的键值或一个其他类型键中的某个元素时就会得到一个字符串回复。字符串回复以双引号包裹:
redis>GET foo
"1"
特殊情况是当请求的键值不存在时会得到一个空结果,显示为(nil)
。如:
redis>GET noexists
(nil)
多行字符串回复(multi-bulk reply)同样很常见,如当请求一个非字符串类型键的元素列表时就会收到多行字符串回复。多行字符串回复中的每行字符串都以一个序号开头,如:
redis>KEYS *
1) "bar"
2) "foo"
提示
KEYS命令的作用是获取数据库中符合指定规则的键名,由于读者的Redis中还没有存储数据,所以得到的返回值应该是(
empty list or set
)。3.1节会具体介绍KEYS命令,此处读者只需了解多行字符串回复的格式即可。
2.2.1节中我们通过redis-server的启动参数port
设置了Redis的端口号,除此之外Redis还支持其他配置选项,如是否开启持久化、日志级别等。由于可以配置的选项较多,通过启动参数设置这些选项并不方便,所以Redis支持通过配置文件来设置这些选项。启用配置文件的方法是在启动时将配置文件的路径作为启动参数传递给redis-server,如:
$redis-server /path/to/redis.conf
通过启动参数传递同名的配置选项会覆盖配置文件中相应的参数,就像这样:
$redis-server /path/to/redis.conf --loglevel warning
Redis提供了一个配置文件的模板redis.conf,位于源代码目录的根目录中。
除此之外还可以在Redis运行时通过CONFIG SET
命令在不重新启动Redis的情况下动态修改部分Redis配置。就像这样:
redis>CONFIG SET loglevel warning
OK
并不是所有的配置都可以使用CONFIG SET
命令修改,附录B列出了哪些配置能够使用该命令修改。同样在运行的时候也可以使用CONFIG GET
命令获得Redis当前的配置情况,如:
redis>CONFIG GET loglevel
1) "loglevel"
2) "warning"
其中第一行字符串回复表示的是选项名,第二行即是选项值。
第1章介绍过Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。
每个数据库对外都是以一个从0开始的递增数字命名,Redis默认支持16个数据库,可以通过配置参数databases
来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT
命令更换数据库,如要选择1号数据库:
redis>SELECT 1
OK
redis [1]>GET foo
(nil)
然而这些以数字命名的数据库又与我们理解的数据库有所区别。首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据。另外Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL
命令可以清空一个Redis实例中所有数据库中的数据。综上所述,这些数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库存储B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内存只有1MB左右,所以不用担心多个Redis实例会额外占用很多内存。
学会如何安装和运行Redis,并了解Redis的基础知识后,本章将详细介绍Redis的5种主要数据类型及相应的命令,带领读者真正进入Redis的世界。在学习的时候,手边打开一个redis-cli程序来跟着一起输入命令将会极大地提高学习效率。尽管在目前多数公司和团队的Redis的应用是以缓存和队列为主。
在之后的章节中你会遇到两个学习伙伴:小白和宋老师。小白是一个标准的极客,最近刚开始他的Redis学习之旅,而他大学时的计算机老师宋老师恰好对Redis颇有研究,于是就顺理成章地成了小白的私人Redis教师。这不,小白想基于Redis开发一个博客,于是找到宋老师,向他请教。在本章中宋老师会向小白介绍Redis最核心的内容—数据类型,从他们的对话中你一定能学到不少知识!
3.2节到3.6节这5节将分别介绍Redis的5种数据类型,其中每节都是由4个部分组成,依次是“介绍”、“命令”、“实践”和“命令拾遗”。“介绍”部分是对数据类型的概述,“命令”部分会对“实践”部分将用到的命令进行介绍,“实践”部分会讲解该数据类型在开发中的应用方法,“命令拾遗”部分会对该数据类型其他比较有用的命令进行补充介绍。
在介绍Redis的数据类型之前,我们先来了解几个比较基础的命令作为热身,赶快打开redis-cli,跟着样例亲自输入命令来体验一下吧!
KEYS pattern
pattern
支持glob风格通配符格式,具体规则如表3-1所示。
表3-1 glob风格通配符规则
符 号 |
含 义 |
---|---|
|
匹配一个字符 |
|
匹配任意个(包括0个)字符 |
|
匹配括号间的任一字符,可以使用“-”符号表示一个范围,如 |
|
匹配字符 |
现在Redis中空空如也(如果你从第2章开始就一直跟着本书的进度输入命令,此时数据库中可能还会有个foo
键),为了演示KEYS
命令,首先我们得给Redis加点料。使用SET
命令(会在3.2节介绍)建立一个名为bar
的键:
redis>SET bar 1
OK
然后使用KEYS *就能
获得Redis中所有的键了(当然由于数据库中只有一个bar键,所以KEYS ba*
或者KEYS bar等命令
都能获得同样的结果):
redis>KEYS *
1) "bar"
注意
KEYS命令需要遍历Redis中的所有键,当键的数量较多时会影响性能,不建议在生产环境中使用。
提示
Redis不区分命令大小写,但在本书中均会使用大写字母表示Redis命令。
EXISTS key
如果键存在则返回整数类型1,否则返回0。例如:
redis>EXISTS bar
(integer) 1
redis> EXISTS noexists
(integer) 0
DEL key [key …]
可以删除一个或多个键,返回值是删除的键的个数。例如:
redis>DEL bar
(integer) 1
redis>DEL bar
(integer) 0
第二次执行DEL
命令时因为bar
键已经被删除了,实际上并没有删除任何键,所以返回0。
技巧
DEL命令的参数不支持通配符,但我们可以结合Linux的管道和
xargs
命令自己实现删除所有符合规则的键。比如要删除所有以“user:
”开头的键,就可以执行*redis-cli KEYS "user:*"|xargs redis-cli DEL
。另外由于DEL
命令支持多个键作为参数,所以还可以执行redis-cli DEL``redis-cli KEYS "user:"
来达到同样的效果,但是性能更好。
TYPE key
TYPE
命令用来获得键值的数据类型,返回值可能是string
(字符串类型)、hash
(散列类型)、list
(列表类型)、set
(集合类型)、zset
(有序集合类型)。例如:
redis>SET foo 1
OK
redis>TYPE foo
string
redis>LPUSH bar 1
(integer) 1
redis>TYPE bar
list
LPUSH
命令的作用是向指定的列表类型键中增加一个元素,如果键不存在则创建它,3.4节会详细介绍。
作为一个爱造轮子的资深极客,小白每次看到自己博客最下面的“Powered by WordPress”1都觉得有些不舒服,终于有一天他下定决心要开发一个属于自己的博客。但是用腻了MySQL数据库的小白总想尝试一下新技术,恰好上次参加Node Party时听人介绍过Redis数据库,便想着趁机试一试。可小白只知道Redis是一个键值对数据库,其他的一概不知。抱着试一试的态度,小白找到了自己大学时教计算机的宋老师,一问之下欣喜地发现宋老师竟然对Redis颇有研究。宋老师有感于小白的好学,决定给小白开个小灶。
1即“由WordPress驱动”。WordPress是一个开源的博客程序,用户可以借其通过简单的配置搭建一个博客或内容管理系统。
小白:
宋老师您好,我最近听别人介绍过Redis,当时就对它很感兴趣。恰好最近想开发一个博客,准备尝试一下它。有什么能快速学会Redis的方法吗?
宋老师笑着说:
心急吃不了热豆腐,要学会Redis就要先掌握Redis的键值数据类型和相关的命令,这些内容是Redis的基础。为了让你更全面地了解Redis的每种数据类型,接下来我会先讲解如何将Redis作为数据库使用,但是实际上Redis可不只是数据库这么简单,更多的公司和团队将Redis用作缓存和队列系统,而这部分内容等你掌握了Redis的基础后我会再进行介绍。作为开始,我先来讲讲Redis中最基本的数据类型—字符串类型。
字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。你可以用其存储用户的邮箱、JSON 化的对象甚至是一张图片。一个字符串类型键允许存储的数据的最大容量是512 MB2。
2 Redis的作者考虑过让字符串类型键支持超过 512 MB大小的数据,未来的版本也可能会放宽这一限制,但无论如何,考虑到Redis的数据是使用内存存储的,512 MB的限制已经非常宽松了。
字符串类型是其他4种数据类型的基础,其他数据类型和字符串类型的差别从某种角度来说只是组织字符串的形式不同。例如,列表类型是以列表的形式组织字符串,而集合类型是以集合的形式组织字符串。学习过本章后面几节后相信读者对此会有更深的理解。
SET key value
GET key
SET
和GET
是Redis中最简单的两个命令,它们实现的功能和编程语言中的读写变量相似,如key = "hello"
在Redis中是这样表示的:
redis> SET key hello
OK
想要读取键值则更简单:
redis> GET key
"hello"
当键不存在时会返回空结果。
为了节约篇幅,同时避免读者过早地被编程语言的细节困扰,本书大部分章节将只使用redis-cli进行命令演示(必要的时候会配合伪代码),第5章会专门介绍在各种编程语言(PHP、Python、Ruby和Node.js)中使用Redis的方法。
不过,为了能让读者提前对Redis命令在实际开发时的用法有一个直观的体会,这里会先使用PHP实现一个SET
/GET
命令的示例网页:用户访问示例网页时程序会通过GET
命令判断Redis中是否存储了用户的姓名,如果有则直接将姓名显示出来(如图3-1所示),如果没有则会提示用户填写(如图3-2所示),用户单击“提交”按钮后程序会使用SET
命令将用户的姓名存入到Redis中。
图3-1 设置过姓名时的页面
图3-2 没有设置过姓名时的页面
代码如下:
<?php
// 加载Predis库的自动加载函数
require './predis/autoload.php';
// 连接Redis
$redis= new Predis\Client(array(
'host' => '127.0.0.1',
'port' => 6379
));
// 如果提交了姓名则使用SET命令将姓名写入到Redis中
if ($_GET['name']) {
$redis->set('name', $_GET['name']);
}
// 通过GET命令从Redis中读取姓名
$name = $redis->get('name');
?><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>我的第一个Redis程序</title>
</head>
<body>
<?php if ($name): ?>
<p>您的姓名是:<?php echo $name; ?></p>
<?php else: ?>
<p>您还没有设置姓名。</p>
<?php endif; ?>
<hr />
<h1>更改姓名</h1>
<form>
<p>
<label for="name">您的姓名:</label>
<input type="text" name="name" id="name" />
</p>
<p>
<button type="submit">提交</button>
</p>
</form>
</body>
</html>
在这个例子中我们使用PHP的Redis客户端库Predis与Redis通信。5.1节会专门介绍Predis,有兴趣的读者可以先跳到5.1节查看Predis的安装方法来实际运行这个例子。
Redis的其他命令也可以使用Predis通过同样的方式调用,如马上要介绍的INCR
命令的调用方法是$redis->incr
(键名)。
INCR key
前面说过字符串类型可以存储任何形式的字符串,当存储的字符串是整数形式时,Redis提供了一个实用的命令INCR
,其作用是让当前键值递增,并返回递增后的值,用法为:
redis>INCR num
(integer) 1
redis>INCR num
(integer) 2
当要操作的键不存在时会默认键值为0,所以第一次递增后的结果是1。当键值不是整数时Redis会提示错误:
redis>SET foo lorem
OK
redis>INCR foo
(error) ERR value is not an integer or out of range
有些读者会想到可以借助GET
和SET
两个命令自己实现incr
函数,伪代码如下:
def incr($key)
$value =GET $key
if not $value
$value = 0
$value = $value + 1
SET $key, $value
return $value
如果Redis同时只连接了一个客户端,那么上面的代码没有任何问题(其实还没有加入错误处理,不过这并不是此处讨论的重点)。可当同一时间有多个客户端连接到Redis时则有可能出现竞态条件(race condition)3。例如有两个客户端A和B都要执行我们自己实现的incr
函数并准备将同一个键的键值递增,当它们恰好同时执行到代码第二行时二者读取到的键值是一样的,如“5”,而后它们各自将该值递增到“6”并使用SET
命令将其赋给原键,结果虽然对键执行了两次递增操作,最终的键值却是“6”而不是预想中的“7”。包括INCR
在内的所有Redis命令都是原子操作(atomic operation)4,无论多少个客户端同时连接,都不会出现上述情况。之后我们还会介绍利用事务(4.1节)和脚本(第6章)实现自定义的原子操作的方法。
3竞态条件是指一个系统或者进程的输出,依赖于不受控制的事件的出现顺序或者出现时机。
4原子操作取“原子”的“不可拆分”的意思,原子操作是最小的执行单位,不会在执行的过程中被其他命令插入打断。
博客的一个常见的功能是统计文章的访问量,我们可以为每篇文章使用一个名为post:
文章ID:page.view
的键来记录文章的访问量,每次访问文章的时候使用INCR
命令使相应的键值递增。
提示
Redis对于键的命名并没有强制的要求,但比较好的实践是用“对象类型:对象ID:对象属性”来命名一个键,如使用键
user:1:friends
来存储ID为1的用户的好友列表。对于多个单词则推荐使用“.”分隔,一方面是沿用以前的习惯(Redis以前版本的键名不能包含空格等特殊字符),另一方面是在redis-cli中容易输入,无需使用双引号包裹。另外为了日后维护方便,键的命名一定要有意义,如u:1:f的可读性显然不如user:1:friends
好(虽然采用较短的名称可以节省存储空间,但由于键值的长度往往远远大于键名的长度,所以这部分的节省大部分情况下并不如可读性来得重要)。
那么怎么为每篇文章生成一个唯一ID呢?在关系数据库中我们通过设置字段属性为AUTO_INCREMENT
来实现每增加一条记录自动为其生成一个唯一的递增ID的目的,而在Redis中可以通过另一种模式来实现:对于每一类对象使用名为对象类型(复数形式):count5的键(如users:count
)来存储当前类型对象的数量,每增加一个新对象时都使用INCR
命令递增该键的值。由于使用INCR
命令建立的键的初始键值是1,所以可以很容易得知,INCR
命令的返回值既是加入该对象后的当前类型的对象总数,又是该新增对象的ID。
5这个键名只是参考命名,实际应用中可以使用任何容易理解的名称。
由于每个字符串类型键只能存储一个字符串,而一篇博客文章是由标题、正文、作者与发布时间等多个元素构成的。为了存储这些元素,我们需要使用序列化函数(如PHP中的serialize
和JavaScript中的JSON.stringify
)将它们转换成一个字符串。除此之外因为字符串类型键可以存储二进制数据,所以也可以使用MessagePack6进行序列化,速度更快,占用空间也更小。
6 MessagePack和JSON一样可以将对象序列化成字符串,但其性能更高,序列化后的结果占用空间更小,序列化后的结果是二进制格式。MessagePack的项目地址是http://msgpack.org。
至此我们已经可以写出发布新文章时与Redis操作相关的伪代码了:
# 首先获得新文章的ID
$postID =INCR posts:count
# 将博客文章的诸多元素序列化成字符串
$serializedPost = serialize($title, $content, $author, $time)
# 把序列化后的字符串存一个入字符串类型的键中
SET post:$postID:data, $serializedPost
获取文章数据的伪代码如下(以访问ID为42的文章为例):
# 从Redis中读取文章数据
$serializedPost =GET post:42:data
# 将文章数据反序列化成文章的各个元素
$title, $content, $author, $time = unserialize($serializedPost)
# 获取并递增文章的访问数量
$count =INCR post:42:page.view
除了使用序列化函数将文章的多个元素存入一个字符串类型键中外,还可以对每个元素使用一个字符串类型键来存储,这种方法会在3.3.3节讨论。
INCRBY key increment
INCRBY
命令与INCR
命令基本一样,只不过前者可以通过increment
参数指定一次增加的数值,如:
redis>INCRBY bar 2
(integer) 2
redis>INCRBY bar 3
(integer) 5
DECR key
DECRBY key decrement
DECR
命令与INCR
命令用法相同,只不过是让键值递减,例如:
redis>DECR bar
(integer) 4
而DECRBY
命令的作用不用介绍想必读者就可以猜到,DECRBY key 5
相当于INCRBY key –5
。
INCRBYFLOAT key increment
INCRBYFLOAT
命令类似INCRBY
命令,差别是前者可以递增一个双精度浮点数,如:
redis>INCRBYFLOAT bar 2.7
"6.7"
redis>INCRBYFLOAT bar 5E+4
"50006.69999999999999929"
APPEND key value
APPEND
作用是向键值的末尾追加value
。如果键不存在则将该键的值设置为value
,即相当于SET key value
。返回值是追加后字符串的总长度。如:
redis>SET key hello
OK
redis>APPEND key " world!"
(integer) 12
此时key
的值是"hello world!"
。APPEND
命令的第二个参数加了双引号,原因是该参数包含空格,在redis-cli中输入需要双引号以示区分。
STRLEN key
STRLEN
命令返回键值的长度,如果键不存在则返回0。例如:
redis>STRLEN key
(integer) 12
redis>SET key 你好
OK
redis>STRLEN key
(integer) 6
前面提到了字符串类型可以存储二进制数据,所以它可以存储任何编码的字符串。例子中Redis接收到的是使用UTF-8编码的中文,由于“你”和“好”两个字的UTF-8编码的长度都是3,所以此例中会返回6。
MGET key [key …]
MSET key value [key value …]
MGET/MSET
与GET/SET
相似,不过MGET/MSET
可以同时获得/设置多个键的键值。例如:
redis>MSET key1 v1 key2 v2 key3 v3
OK
redis>GET key2
"v2"
redis>MGET key1 key3
1) "v1"
2) "v3"
GETBIT key offset
SETBIT key offset value
BITCOUNT key [start] [end]
BITOP operation destkey key [key …]
一个字节由8个二进制位组成,Redis提供了4个命令可以直接对二进制位进行操作。为了演示,我们首先将foo
键赋值为bar
:
redis>SET foo bar
OK
bar
的3个字母“b”“a”和“r”对应的ASCII码分别为98、97和114,转换成二进制后分别为1100010、1100001和1110010,所以foo
键中的二进制位结构如图3-3所示。
图3-3 bar
的二进制存储结构
GETBIT
命令可以获得一个字符串类型键指定位置的二进制位的值(0或1),索引从0开始:
redis>GETBIT foo 0
(integer) 0
redis>GETBIT foo 6
(integer) 1
如果需要获取的二进制位的索引超出了键值的二进制位的实际长度则默认位值是0:
redis>GETBIT foo 100000
(integer) 0
SETBIT
命令可以设置字符串类型键指定位置的二进制位的值,返回值是该位置的旧值。如我们要将foo
键值设置为aar
,可以通过位操作将foo
键的二进制位的索引第6位设为0,第7位设为1:
redis>SETBIT foo 6 0
(integer) 1
redis>SETBIT foo 7 1
(integer) 0
redis>GET foo
"aar"
如果要设置的位置超过了键值的二进制位的长度,SETBIT命令会自动将中间的二进制位设置为0,同理设置一个不存在的键的指定二进制位的值会自动将其前面的位赋值为0:
redis>SETBIT nofoo 10 1
(integer) 0
redis>GETBIT nofoo 5
(integer) 0
BITCOUNT
命令可以获得字符串类型键中值是1的二进制位个数,例如:
redis>BITCOUNT foo
(integer) 10
可以通过参数来限制统计的字节范围,如我们只希望统计前两个字节(即"aa
"):
redis>BITCOUNT foo 0 1
(integer) 6
BITOP
命令可以对多个字符串类型键进行位运算,并将结果存储在destkey
参数指定的键中。BITOP
命令支持的运算操作有AND
、OR
、XOR
和NOT
。如我们可以对bar
和aar
进行OR
运算:
redis>SET foo1 bar
OK
redis>SET foo2 aar
OK
redis>BITOP OR res foo1 foo2
(integer) 3
redis> GET res
"car"
运算过程如图3-4所示。
图3-4 OR
运算过程示意
Redis 2.8.7引入了BITPOS
命令,可以获得指定键的第一个位值是0或者1的位置。还是以“bar”这个键值为例,如果想获取键值中的第一个二进制位为1的偏移量,则可以执行:
redis>SET foo bar
OK
redis>BITPOS foo 1
(integer) 1
结合图3-3可以看出,正如BITPOS命令的
结果所示,“bar”中的第一个值为1的二进制位的偏移量为1(同其他命令一样,BITPO
S命令的索引也是从0
开始算起)。那么有没有可能指定二进制位的查询范围呢?BITPOS
命令的第二个和第三个参数分别可以用来指定要查询的起始字节(同样从0开始算起)和结束字节。注意这里的单位不再是二进制位,而是字节。如果我们想查询第二个字节到第三个字节之间(即“a”和“r”)出现的第一个值为1的二进制位的偏移量,则可以执行:
redis>BITPOS foo 1 1 2
(integer) 9
这里的返回结果的偏移量是从头开始算起的,与起始字节无关。另外要特别说明的一个有趣的现象是如果不设置结束字节且键值的所有二进制位都是1,则当要查询值为0的二进制位偏移量时,返回结果会是键值长度的下一个字位的偏移量。这是因为Redis会认为键值长度之后的二进制位都是0。
利用位操作命令可以非常紧凑地存储布尔值。比如如果网站的每个用户都有一个递增的整数ID,如果使用一个字符串类型键配合位操作来记录每个用户的性别(用户ID作为索引,二进制位值1和0表示男性和女性),那么记录100万个用户的性别只需占用100 KB多的空间,而且由于GETBIT
和SETBIT
的时间复杂度都是O(1),所以读取二进制位值性能很高。
注意
使用SETBIT命令时,如果当前键的键值长度小于要设置的二进制位的偏移量时,Redis会自动分配内存并将键值的当前长度到指定的偏移量之间的二进制位都设置为0。如果要分配的内存过大,则很可能会造成服务器的暂时阻塞而无法接收同一时间的其他请求。举例而言,在一台2014年的MacBook Pro笔记本上,设置偏移量232-1的值(即分配500 MB的内存)需要耗费将近1秒的时间。分配过大的偏移量除了会造成服务器阻塞,还会造成空间浪费。还是举刚才存储网站用户性别的例子,如果这个网站的用户ID是从100000001开始的,那么会造成10多MB的浪费,正确的做法是给每个用户的ID减去100000000再进行存储。
小白只用了半个多小时就把访问统计和发表文章两个部分做好了。同时借助Bootstrap框架,老师花了一小会儿时间教会了之前只涉猎过HTML的小白如何做出一个像样的网页界面。
接着小白发问:
接下来我想要做的功能是博客的文章列表页,我设想在列表页中每个文章只显示标题部分,可是使用您刚才介绍的方法,若想取得文章的标题,必须把整个文章数据字符串取出来反序列化,而其中占用空间最大的文章内容部分却是不需要的,这样难道不会在传输和处理时造成资源浪费吗?
老师有些惊喜地看着小白答道:“很对!”同时以一个夸张的幅度点了下头,接着说:
这正是我接下来准备讲的。不仅取数据时会有资源浪费,在修改数据时也会有这个问题,比如当你只想更改文章的标题时也不得不把整个文章数据字符串更新一遍。
没等小白再问,老师就又继续说道:
前面我说过Redis的强大特性之一就是提供了多种实用的数据类型,其中的散列类型可以非常好地解决这个问题。
我们现在已经知道Redis是采用字典结构以键值对的形式存储数据的,而散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他数据类型,换句话说,散列类型不能嵌套其他的数据类型。一个散列类型键可以包含至多232−1个字段。
提示
除了散列类型,Redis 的其他数据类型同样不支持数据类型嵌套。比如集合类型的每个元素都只能是字符串,不能是另一个集合或散列表等。
散列类型适合存储对象:使用对象类别和ID构成键名,使用字段表示对象的属性,而字段值则存储属性值。例如要存储ID为2的汽车对象,可以分别使用名为color
、name
和price
的3个字段来存储该辆汽车的颜色、名称和价格。存储结构如图3-5所示。
图3-5 使用散列类型存储汽车对象的结构图
回想在关系数据库中如果要存储汽车对象,存储结构如表3-2所示。
表3-2 关系数据库存储汽车资料的表结构
ID |
color |
name |
price |
---|---|---|---|
1 |
黑色 |
宝马 |
100万 |
2 |
白色 |
奥迪 |
90万 |
3 |
蓝色 |
宾利 |
600万 |
数据是以二维表的形式存储的,这就要求所有的记录都拥有同样的属性,无法单独为某条记录增减属性。如果想为ID为1的汽车增加生产日期属性,就需要把数据表更改为如表3-3所示的结构。
表3-3 为其中一辆汽车增加一个“属性”
ID |
color |
name |
price |
date |
---|---|---|---|---|
1 |
黑色 |
宝马 |
100万 |
2012年12月21日 |
2 |
白色 |
奥迪 |
90万 |
|
3 |
蓝色 |
宾利 |
600万 |
|
对于ID为2和3的两条记录而言date
字段是冗余的。可想而知当不同的记录需要不同的属性时,表的字段数量会越来越多以至于难以维护。而且当使用ORM8将关系数据库中的对象实体映射成程序中的实体时,修改表的结构往往意味着要中断服务(重启网站程序)。为了防止这些问题,在关系数据库中存储这种半结构化数据还需要额外的表才行。
8即Object-Relational Mapping(对象关系映射)。
而Redis的散列类型则不存在这个问题。虽然我们在图3-5中描述了汽车对象的存储结构,但是这个结构只是人为的约定,Redis并不要求每个键都依据此结构存储,我们完全可以自由地为任何键增减字段而不影响其他键。
HSET key field value
HGET key field
HMSET key field value [field value …]
HMGET key field [field …]
HGETALL key
HSET
命令用来给字段赋值,而HGET
命令用来获得字段的值。用法如下:
redis>HSET car price 500
(integer) 1
redis>HSET car name BMW
(integer) 1
redis>HGET car name
"BMW"
HSET
命令的方便之处在于不区分插入和更新操作,这意味着修改数据时不用事先判断字段是否存在来决定要执行的是插入操作(update)还是更新操作(insert)。当执行的是插入操作时(即之前字段不存在)HSET
命令会返回1,当执行的是更新操作时(即之前字段已经存在)HSET
命令会返回0。更进一步,当键本身不存在时,HSET
命令还会自动建立它。
提示
在Redis中每个键都属于一个明确的数据类型,如通过HSET命令建立的键是散列类型,通过SET命令建立的键是字符串类型等等。使用一种数据类型的命令操作另一种数据类型的键会提示错误:
"ERR Operation against a key holding the wrong kind of value"
9。
9并不是所有命令都是如此,比如SET命令可以覆盖已经存在的键而不论原来键是什么类型。
当需要同时设置多个字段的值时,可以使用HMSET
命令。例如,下面两条语句
HSET key field1 value1
HSET key field2 value2
可以用HMSET
命令改写成
HMSET key field1 value1 field2 value2
相应地,HMGET
命令可以同时获得多个字段的值:
redis>HMGET car price name
1) "500"
2) "BMW"
如果想获取键中所有字段和字段值却不知道键中有哪些字段时(如3.3.1节介绍的存储汽车对象的例子,每个对象拥有的属性都未必相同)应该使用HGETALL
命令。如:
redis>HGETALL car
1) "price"
2) "500"
3) "name"
4) "BMW"
返回的结果是字段和字段值组成的列表,不是很直观,好在很多语言的Redis客户端会将 HGETALL
的返回结果封装成编程语言中的对象,处理起来就非常方便了。例如,在Node.js中:
redis.hgetall("car", function (error, car) {
// hgetall方法的返回的值被封装成了JavaScript的对象
console.log(car.price);
console.log(car.name);
});
HEXISTS key field
HEXISTS
命令用来判断一个字段是否存在。如果存在则返回1,否则返回0(如果键不存在也会返回0)。
redis>HEXISTS car model
(integer) 0
redis>HSET car model C200
(integer) 1
redis>HEXISTS car model
(integer) 1
HSETNX key field value
HSETNX
10命令与
HSET命令类似,区别在于如果字段已经存在,
HSETNX`命令将不执行任何操作。其实现可以表示为如下伪代码:
10HSETNX
中的“NX”表示“if Note Xists”(如果不存在)。`
def hsetnx($key, $field, $value)
$isExists =HEXISTS $key, $field
if $isExists is 0
HSET $key, $field, $value
return 1
else
return 0
只不过HSETNX
命令是原子操作,不用担心竞态条件。
HINCRBY key field increment
上一节的命令拾遗部分介绍了字符串类型的命令INCRBY
,HINCRBY
命令与之类似,可以使字段值增加指定的整数。散列类型没有HINCR
命令,但是可以通过HINCRBY key field 1
来实现。
HINCRBY
命令的示例如下:
redis>HINCRBY person score 60
(integer) 60
之前person
键不存在,HINCRBY
命令会自动建立该键并默认score
字段在执行命令前的值为“0”。命令的返回值是增值后的字段值。
HDEL key field [field …]
HDEL
命令可以删除一个或多个字段,返回值是被删除的字段个数:
redis>HDEL car price
(integer) 1
redis>HDEL car price
(integer) 0
3.2.3节介绍了可以将文章对象序列化后使用一个字符串类型键存储,可是这种方法无法提供对单个字段的原子读写操作支持,从而产生竞态条件,如两个客户端同时获得并反序列化某个文章的数据,然后分别修改不同的属性后存入,显然后存入的数据会覆盖之前的数据,最后只会有一个属性被修改。另外如小白所说,即使只需要文章标题,程序也不得不将包括文章内容在内的所有文章数据取出并反序列化,比较消耗资源。
除此之外,还有一种方法是组合使用多个字符串类型键来存储一篇文章的数据,如图3-6所示。
图3-6 使用多个字符串类型键存储一个对象
使用这种方法的好处在于无论获取还是修改文章数据,都可以只对某一属性进行操作,十分方便。而本章介绍的散列类型则更适合此场景,使用散列类型的存储结构如图3-7所示。
从图3-7可以看出使用散列类型存储文章数据比图3-6所示的方法看起来更加直观,也更容易维护(比如可以使用HGETALL
命令获得一个对象的所有字段,删除一个对象时只需要删除一个键),另外存储同样的数据散列类型往往比字符串类型更加节约空间,具体的细节会在4.6节中介绍。
使用过WordPress的读者可能会知道发布文章时一般需要指定一个缩略名(slug)来构成该篇文章的网址的一部分,缩略名必须符合网址规范且最好可以与文章标题含义相似,如“This Is A Great Post!”的缩略名可以为“this-is-a-great-post”。每个文章的缩略名必须是唯一的,所以在发布文章时程序需要验证用户输入的缩略名是否存在,同时也需要通过缩略名获得文章的ID。
图3-7 使用一个散列类型键存储一个对象
我们可以使用一个散列类型的键slug.to.id
来存储文章缩略名和ID之间的映射关系。其中字段用来记录缩略名,字段值用来记录缩略名对应的ID。这样就可以使用HEXISTS
命令来判断缩略名是否存在,使用HGET
命令来获得缩略名对应的文章ID了。
现在发布文章可以修改成如下代码:
$postID =INCR posts:count
# 判断用户输入的slug是否可用,如果可用则记录
$isSlugAvailable =HSETNX slug.to.id, $slug, $postID
if $isSlugAvailable is 0
# slug已经用过了,需要提示用户更换slug,
# 这里为了演示方便直接退出。
exit
HMSET post:$postID, title, $title, content, $content, slug, $slug,...
这段代码使用了HSETNX
命令原子地实现了HEXISTS
和HSET
两个命令以避免竞态条件。当用户访问文章时,我们从网址中得到文章的缩略名,并查询slug.to.id
键来获取文章ID:
$postID =HGET slug.to.id, $slug
if not $postID
print 文章不存在
exit
$post =HGETALL post:$postID
print 文章标题:$post.title
需要注意的是如果要修改文章的缩略名一定不能忘了修改slug.to.id
键对应的字段。如要修改ID为42的文章的缩略名为newSlug
变量的值:
# 判断新的slug是否可用,如果可用则记录
$isSlugAvailable =HSETNX slug.to.id, $newSlug, 42
if $isSlugAvailable is 0
exit
# 获得旧的缩略名
$oldSlug =HGET post:42, slug
# 设置新的缩略名
HSET post:42, slug, $newSlug
# 删除旧的缩略名
HDEL slug.to.id, $oldSlug
HKEYS key
HVALS key
有时仅仅需要获取键中所有字段的名字而不需要字段值,那么可以使用HKEYS
命令,就像这样:
redis>HKEYS car
1) "name"
2) "model"
HVALS
命令与HKEYS
命令相对应,HVALS
命令用来获得键中所有字段值,例如:
redis>HVALS car
1) "BMW"
2) "C200"
HLEN key
例如:
redis>HLEN car
(integer) 2
正当小白踌躇满志地写着文章列表页的代码时,一个很重要的问题阻碍了他的开发,于是他请来了宋老师为他讲解。
原来小白是使用如下流程获得文章列表的:
posts:count
键获得博客中最大的文章ID;ID - (n - 1) * 10"
到"max(
最大的文章ID - n * 10 + 1, 1)"
;HMGET
命令来获得文章数据。对应的伪代码如下:
# 每页显示10篇文章
$postsPerPage = 10
# 获得最后发表的文章ID
$lastPostID =GET posts:count
# $currentPage存储的是当前页码,第一页时$currentPage的值为1,依此类推
$start = $lastPostID - ($currentPage - 1) * $postsPerPage
$end = max($lastPostID - $currentPage * $postsPerPage + 1, 1)
# 遍历文章ID获取数据
for $i = $start down to $end
# 获取文章的标题和作者并打印出来
post =HMGET post:$i, title, author
print $post[0] # 文章标题
print $post[1] # 文章作者
可是这种方式要求用户不能删除文章以保证 ID 连续,否则小白就必须在程序中使用EXISTS
命令判断某个ID的文章是否存在,如果不存在则跳过。由于每删除一篇文章都会影响后面的页码分布,为了保证每页的文章列表都能正好显示10篇文章,不论是第几页,都不得不从最大的文章ID开始遍历来获得当前页面应该显示哪些文章。
小白摇了摇头,心想:“真是个灾难!”然后看向宋老师,试探地问道:“我想到了KEYS
命令,可不可以使用KEYS
命令获得所有以“post:
”开头的键,然后再根据键名分页呢?”
宋老师回答道:“确实可行,不过KEYS
命令需要遍历数据库中的所有键,出于性能考虑一般很少在生产环境中使用这个命令。至于你提到的问题,可以使用Redis的列表类型来解决。”
列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的(和从只有20个元素的列表中获取头部或尾部的10条记录的速度是一样的)。
不过使用链表的代价是通过索引访问元素比较慢,设想在iPad mini发售当天有1000个人在三里屯的苹果店排队等候购买,这时苹果公司宣布为了感谢大家的排队支持,决定奖励排在第486位的顾客一部免费的iPad mini。为了找到这第486位顾客,工作人员不得不从队首一个一个地数到第486个人。但同时,无论队伍多长,新来的人想加入队伍的话直接排到队尾就好了,和队伍里有多少人没有任何关系。这种情景与列表类型的特性很相似。
这种特性使列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,我们关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的100条数据也是极快的。同样因为在两端插入记录的时间复杂度是O(1),列表类型也适合用来记录日志,可以保证加入新日志的速度不会受到已有日志数量的影响。
借助列表类型,Redis还可以作为队列使用,4.4节会详细介绍。
与散列类型键最多能容纳的字段数量相同,一个列表类型键最多能容纳232−1个元素。
LPUSH key value [value …]
RPUSH key value [value …]
LPUSH
命令用来向列表左边增加元素,返回值表示增加元素后列表的长度。
redis>LPUSH numbers 1
(integer) 1
这时numbers
键中的数据如图3-8所示。LPUSH
命令还支持同时增加多个元素,例如:
redis>LPUSH numbers 2 3
(integer) 3
LPUSH
会先向列表左边加入"2"
,然后再加入"3"
,所以此时numbers
键中的数据如图3-9所示。
图3-8 加入元素1后numbers
键中的数据
图3-9 加入元素2,3后numbers
键中的数据
向列表右边增加元素的话则使用RPUSH
命令,其用法和LPUSH
命令一样:
redis>RPUSH numbers 0 −1
(integer) 5
此时numbers键中的数据如图3-10所示。
图3-10 使用RPUSH
命令加入元素0,-1后numbers
键中的数据
LPOP key
RPOP key
有进有出,LPOP
命令可以从列表左边弹出一个元素。LPOP
命令执行两步操作:第一步是将列表左边的元素从列表中移除,第二步是返回被移除的元素值。例如,从numbers列表左边弹出一个元素(也就是"3"
):
redis>LPOP numbers
"3"
此时numbers
键中的数据如图3-11所示。
同样,RPOP命令
可以从列表右边弹出一个元素:
redis>RPOP numbers
"-1"
此时numbers
键中的数据如图3-12所示。
结合上面提到的4个命令可以使用列表类型来模拟栈和队列的操作:如果想把列表当作栈,则搭配使用LPUSH
和LPOP
或RPUSH
和RPOP
,如果想当成队列,则搭配使用LPUSH
和RPOP
或RPUSH
和LPOP
。
图3-11 从左侧弹出元素后numbers键中的数据
图3-12 从右侧弹出元素后numbers键中的数据
LLEN key
当键不存在时LLEN
会返回0:
redis>LLEN numbers
(integer) 3
LLEN
命令的功能类似SQL语句SELECT COUNT(*) FROM table_name
,但是LLEN
的时间复杂度为O(1),使用时Redis会直接读取现成的值,而不需要像部分关系数据库(如使用InnoDB存储引擎的MySQL表)那样需要遍历一遍数据表来统计条目数量。
LRANGE key start stop
LRANGE
命令是列表类型最常用的命令之一,它能够获得列表中的某一片段。LRANGE
命令将返回索引从start
到stop
之间的所有元素(包含两端的元素)。与大多数人的直觉相同,Redis的列表起始索引为0:
redis>LRANGE numbers 0 2
1) "2"
2) "1"
3) "0"
LRANGE
命令在取得列表片段的同时不会像LPOP
一样删除该片段,另外LRANGE
命令与很多语言中用来截取数组片段的方法slice
有一点区别是LRANGE
返回的值包含最右边的元素,如在JavaScript中:
var numbers = [2, 1, 0];
console.log(numbers.slice(0, 2)); // 返回数组:[2, 1]
LRANGE
命令也支持负索引,表示从右边开始计算序数,如"−1"
表示最右边第一个元素,"-2"
表示最右边第二个元素,依次类推:
redis>LRANGE numbers -2 -1
1) "1"
2) "0"
显然,LRANGE numbers 0 -1
可以获取列表中的所有元素。另外一些特殊情况如下。
1.如果start
的索引位置比stop
的索引位置靠后,则会返回空列表。
2.如果stop
大于实际的索引范围,则会返回到列表最右边的元素:
redis>LRANGE numbers 1 999
1) "1"
2) "0"
LREM key count value
LREM
命令会删除列表中前count
个值为value
的元素,返回值是实际删除的元素个数。根据count
值的不同,LREM
命令的执行方式会略有差异。
(1)当count > 0
时LREM
命令会从列表左边开始删除前count
个值为value
的元素。
(2)当count < 0
时LREM
命令会从列表右边开始删除前|count|
个值为value
的元素。
(3)当count = 0
是LREM
命令会删除所有值为value
的元素。例如:
redis>RPUSH numbers 2
(integer) 4
redis>LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
4) "2"
# 从右边开始删除第一个值为"2"的元素
redis>LREM numbers -1 2
(integer) 1
redis>LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
为了解决小白遇到的问题,我们使用列表类型键posts:list
记录文章ID列表。当发布新文章时使用LPUSH
命令把新文章的ID加入这个列表中,另外删除文章时也要记得把列表中的文章ID删除,就像这样:LREM posts:list 1
要删除的文章ID
有了文章ID列表,就可以使用LRANGE
命令来实现文章的分页显示了。伪代码如下:
$postsPerPage = 10
$start = ($currentPage - 1) * $postsPerPage
$end = $currentPage * $postsPerPage - 1
$postsID =LRANGE posts:list, $start, $end
# 获得了此页需要显示的文章ID列表,我们通过循环的方式来读取文章
for each $id in $postsID
$post =HGETALL post:$id
print 文章标题:$post.title
这样显示的文章列表是根据加入列表的顺序倒序的(即最新发布的文章显示在前面),如果想让最旧的文章显示在前面,可以使用LRANGE
命令获取需要的部分并在客户端中将顺序反转显示出来,具体的实现交由读者来完成。
小白的问题至此就解决了,美中不足的一点是散列类型没有类似字符串类型的MGET命令
那样可以通过一条命令同时获得多个键的键值的版本,所以对于每个文章ID都需要请求一次数据库,也就都会产生一次往返时延(round-trip delay time)11,之后我们会介绍使用管道和脚本来优化这个问题。
114.5节中还会详细介绍这个概念。
另外使用列表类型键存储文章ID列表有以下两个问题。
(1)文章的发布时间不易修改:修改文章的发布时间不仅要修改post:
文章ID中的time
字段,还需要按照实际的发布时间重新排列posts:list
中的元素顺序,而这一操作相对比较繁琐。
(2)当文章数量较多时访问中间的页面性能较差:前面已经介绍过,列表类型是通过链表实现的,所以当列表元素非常多时访问中间的元素效率并不高。
但如果博客不提供修改文章时间的功能并且文章数量也不多时,使用列表类型也不失为一种好办法。对于小白要做的博客系统来讲,现阶段的成果已经足够实用且值得庆祝了。3.6节将介绍使用有序集合类型存储文章ID列表的方法。
在博客中还可以使用列表类型键存储文章的评论。由于小白的博客不允许访客修改自己发表的评论,而且考虑到读取评论时需要获得评论的全部数据(评论者姓名,联系方式,评论时间和评论内容),不像文章一样有时只需要文章标题而不需要文章正文。所以适合将一条评论的各个元素序列化成字符串后作为列表类型键中的元素来存储。
我们使用列表类型键post:文章ID:comments
来存储某个文章的所有评论。发布评论的伪代码如下(以ID为42的文章为例):
# 将评论序列化成字符串
$serializedComment = serialize($author, $email, $time, $content)
LPUSH post:42:comments, $serializedComment
读取评论时同样使用LRANGE
命令即可,具体的实现在此不再赘述。
LINDEX key index
LSET key index value
如果要将列表类型当作数组来用,LINDEX
命令是必不可少的。LINDEX
命令用来返回指定索引的元素,索引从0开始。如:
redis>LINDEX numbers 0
"2"
如果index
是负数则表示从右边开始计算的索引,最右边元素的索引是−1。例如:
redis>LINDEX numbers -1
"0"
LSET
是另一个通过索引操作列表的命令,它会将索引为index
的元素赋值为value
。例如:
redis>LSET numbers 1 7
OK
redis>LINDEX numbers 1
"7"
LTRIM key start end
LTRIM
命令可以删除指定索引范围之外的所有元素,其指定列表范围的方法和LRANGE
命令相同。就像这样:
redis>LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
"0"
redis> LTRIM numbers 1 2
OK
redis>LRANGE numbers 0 1
1) "2"
2) "7"
LTRIM
命令常和LPUSH
命令一起使用来限制列表中元素的数量,比如记录日志时我们希望只保留最近的100条日志,则每次加入新元素时调用一次LTRIM
命令即可:
LPUSH logs $newLog
LTRIM logs 0 99
LINSERT key BEFORE|AFTER pivot value
LINSERT
命令首先会在列表中从左到右查找值为pivot
的元素,然后根据第二个参数是BEFORE
还是AFTER
来决定将value
插入到该元素的前面还是后面。
LINSERT
命令的返回值是插入后列表的元素个数。示例如下:
redis>LRANGE numbers 0 -1
1) "2"
2) "7"
3) "0"
redis>LINSERT numbers AFTER 7 3
(integer) 4
redis>LRANGE numbers 0 -1
1) "2"
2) "7"
3) "3"
4) "0"
redis>LINSERT numbers BEFORE 2 1
(integer) 5
redis>LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
5) "0"
RPOPLPUSH source destination
RPOPLPUSH
是个很有意思的命令,从名字就可以看出它的功能:先执行RPOP
命令再执行LPUSH
命令。RPOPLPUSH
命令会先从source
列表类型键的右边弹出一个元素,然后将其加入到destination
列表类型键的左边,并返回这个元素的值,整个过程是原子的。其具体实现可以表示为伪代码:
def rpoplpush ($source, $destination)
$value =RPOP $source
LPUSH $destination, $value
return $value
当把列表类型作为队列使用时,RPOPLPUSH
命令可以很直观地在多个队列中传递数据。当source
和destination
相同时,RPOPLPUSH
命令会不断地将队尾的元素移到队首,借助这个特性我们可以实现一个网站监控系统:使用一个队列存储需要监控的网址,然后监控程序不断地使用RPOPLPUSH
命令循环取出一个网址来测试可用性。这里使用RPOPLPUSH
命令的好处在于在程序执行过程中仍然可以不断地向网址列表中加入新网址,而且整个系统容易扩展,允许多个客户端同时处理队列。
博客首页,文章页面,评论页面……眼看着博客逐渐成形,小白的心情也是越来越好。时间已经到了深夜,小白却还陶醉于编码之中。不过一个他无法解决的问题最终还是让他不得不提早睡觉去:小白不知道该怎么在Redis中存储文章标签(tag)。他想过使用散列类型或列表类型存储,虽然都能实现,但是总觉得颇有不妥,再加上之前几天领略了Redis的强大功能后,小白相信一定有一种合适的数据类型能满足他的需求。于是小白给宋老师发了封询问邮件后就睡觉去了。
转天一早就收到了宋老师的回复:
你很善于思考嘛!你想的没错,Redis 有一种数据类型很适合存储文章的标签,它就是集合类型。
集合的概念高中的数学课就学习过。在集合中的每个元素都是不同的,且没有顺序。一个集合类型(set)键可以存储至多232 −1个(相信这个数字对大家来说已经很熟悉了)字符串。
集合类型和列表类型有相似之处,但很容易将它们区分开来,如表3-4所示。
表3-4 集合类型和列表类型对比
|
集 合 类 型 |
列 表 类 型 |
---|---|---|
存储内容 |
至多232 −1个字符串 |
至多232 − 1个字符串 |
有序性 |
否 |
是 |
唯一性 |
是 |
否 |
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算,稍后就会看到灵活运用这一特性带来的便利。
SADD key member [member …]
SREM key member [member …]
SADD
命令用来向集合中增加一个或多个元素,如果键不存在则会自动创建。因为在一个集合中不能有相同的元素,所以如果要加入的元素已经存在于集合中就会忽略这个元素。本命令的返回值是成功加入的元素数量(忽略的元素不计算在内)。例如:
redis>SADD letters a
(integer) 1
redis>SADD letters a b c
(integer) 2
第二条SADD
命令的返回值为2是因为元素“a”已经存在,所以实际上只加入了两个元素。
SREM
命令用来从集合中删除一个或多个元素,并返回删除成功的个数,例如:
redis>SREM letters c d
(integer) 1
由于元素“d”在集合中不存在,所以只删除了一个元素,返回值为1。
SMEMBERS key
SMEMBERS
命令会返回集合中的所有元素,例如:
redis>SMEMBERS letters
1) "b"
2) "a"
SISMEMBER key member
判断一个元素是否在集合中是一个时间复杂度为O(1)的操作,无论集合中有多少个元素,SISMEMBER
命令始终可以极快地返回结果。当值存在时SISMEMBER
命令返回1,当值不存在或键不存在时返回0,例如:
redis>SISMEMBER letters a
(integer) 1
redis>SISMEMBER letters d
(integer) 0
SDIFF key [key …]
SINTER key [key …]
SUNION key [key …]
接下来要介绍的3个命令都是用来进行多个集合间运算的。
(1)SDIFF
命令用来对多个集合执行差集运算。集合A与集合B的差集表示为A−B,代表所有属于A且不属于B的元素构成的集合(如图3-13所示),即A−B = {x | x∈A且x∈B}。例如:
{1, 2, 3} - {2, 3, 4} = {1}
{2, 3, 4} - {1, 2, 3} = {4}
SDIFF
命令的使用方法如下:
redis>SADD setA 1 2 3
(integer) 3
redis>SADD setB 2 3 4
(integer) 3
redis> SDIFF setA setB
1) "1"
redis>SDIFF setB setA
1) "4"
SDIFF
命令支持同时传入多个键,例如:
redis>SADD setC 2 3
(integer) 2
redis>SDIFF setA setB setC
1) "1"
计算顺序是先计算setA - setB
,再计算结果与setC
的差集。
(2)SINTER
命令用来对多个集合执行交集运算。集合A与集合B的交集表示为A ∩ B,代表所有属于A且属于B的元素构成的集合(如图3-14所示),即A ∩ B = {x | x ∈ A且x ∈B}。例如:
{1, 2, 3} ∩ {2, 3, 4} = {2, 3}
SINTER
命令的使用方法如下:
redis>SINTER setA setB
1) "2"
2) "3"
SINTER
命令同样支持同时传入多个键,如:
redis>SINTER setA setB setC
1) "2"
2) "3"
(3)SUNION
命令用来对多个集合执行并集运算。集合A与集合B的并集表示为A∪B,代表所有属于A或属于B的元素构成的集合(如图3-15所示)即A∪B = {x | x∈A或x ∈B}。例如:
{1, 2, 3} ∪ {2, 3, 4} = {1, 2, 3, 4}
图3-14 图中斜线部分表示A ∩ B
图3-15 图中斜线部分表示A ∪ B
SUNION
命令的使用方法如下:
redis>SUNION setA setB
1) "1"
2) "2"
3) "3"
4) "4"
SUNION
命令同样支持同时传入多个键,例如:
redis>SUNION setA setB setC
1) "1"
2) "2"
3) "3"
4) "4"
考虑到一个文章的所有标签都是互不相同的,而且展示时对这些标签的排列顺序并没有要求,我们可以使用集合类型键存储文章标签。
对每篇文章使用键名为post:文章ID:tags
的键存储该篇文章的标签。具体操作如伪代码:
# 给ID为42的文章增加标签:
SADD post:42:tags, 闲言碎语, 技术文章, Java
# 删除标签:
SREM post:42:tags, 闲言碎语
# 显示所有的标签:
$tags =SMEMBERS post:42:tags
print $tags
使用集合类型键存储标签适合需要单独增加或删除标签的场合。如在WordPress博客程序中无论是添加还是删除标签都是针对单个标签的(如图3-16所示),可以直观地使用SADD
和SREM
命令完成操作。
另一方面,有些地方需要用户直接设置所有标签后一起上传修改,图3-17所示是某网站的个人资料编辑页面,用户编辑自己的爱好后提交,程序直接覆盖原来的标签数据,整个过程没有针对单个标签的操作,并未利用到集合类型的优势,所以此时也可以直接使用字符串类型键存储标签数据。
图3-16 在WordPress中设置文章标签
图3-17 在百度中设置个人爱好
之所以特意提到这个在实践中的差别是想说明对于Redis存储方式的选择并没有绝对的规则,比如3.4节介绍过使用列表类型存储访客评论,但是在一些特定的场合下散列类型甚至字符串类型可能更适合。
有时我们还需要列出某个标签下的所有文章,甚至需要获得同时属于某几个标签的文章列表,这种需求在传统关系数据库中实现起来比较复杂,下面举一个例子。
现有3张表,即posts
、tags
和posts_tags
,分别存储文章数据、标签、文章与标签的对应关系。结构分别如表3-5、表3-6、表3-7所示。
表3-5 posts
表结构
字 段 名 |
说 明 |
---|---|
|
文章ID |
|
文章标题 |
表3-6 tags
表结构
字 段 名 |
说 明 |
---|---|
|
标签ID |
|
标签名称 |
表3-7 posts_tags
表结构
字 段 名 |
说 明 |
---|---|
|
对应的文章ID |
|
对应的标签ID |
为了找到同时属于“Java”、“MySQL”和“Redis”这3个标签的文章,需要使用如下的SQL语句:
SELECT p.post_title
FROM posts_tags pt,
posts p,
tags t
WHERE pt.tag_id = t.tag_id
AND (t.tag_name IN ('Java', 'MySQL', 'Redis'))
AND p.post_id = pt.post_id
GROUP BY p.post_id HAVING COUNT(p.post_id)=3;
可以很明显看到这样的 SQL 语句不仅效率相对较低,而且不易阅读和维护。而使用Redis可以很简单直接地实现这一需求。
具体做法是为每个标签使用一个名为tag:标签名称:posts
的集合类型键存储标有该标签的文章ID列表。假设现在有3篇文章,ID分别为1、2、3,其中ID为1的文章标签是“Java”,ID为2的文章标签是“Java”、“MySQL”,ID为3的文章标签是“Java”、 “MySQL”和“Redis”,则有关标签部分的存储结构如图3-18所示12。
12集合类型键中元素是无序的,图3-18中为了便于读者阅读将元素按照大小顺序进行了排列。
图3-18 和标签有关部分的存储结构
最简单的,当需要获取标记“MySQL”标签的文章时只需要使用命令 SMEMBERS tag:MySQL:posts
即可。如果要实现找到同时属于Java、MySQL和Redis 3个标签的文章,只需要将tag:Java:posts
、tag:MySQL:posts
和tag:Redis:posts
这3个键取交集,借助SINTER
命令即可轻松完成。
SCARD key
SCARD
命令用来获得集合中的元素个数,例如:
redis>SMEMBERS letters
1) "b"
2) "a"
redis>SCARD letters
(integer) 2
SDIFFSTORE destination key [key …]
SINTERSTORE destination key [key …]
SUNIONSTORE destination key [key …]
SDIFFSTORE
命令和SDIFF
命令功能一样,唯一的区别就是前者不会直接返回运算结果,而是将结果存储在destination
键中。
SDIFFSTORE
命令常用于需要进行多步集合运算的场景中,如需要先计算差集再将结果和其他键计算交集。
SINTERSTORE
和SUNIONSTORE
命令与之类似,不再赘述。
SRANDMEMBER key [count]
SRANDMEMBER
命令用来随机从集合中获取一个元素,如:
redis>SRANDMEMBER letters
"a"
redis>SRANDMEMBER letters
"b"
redis>SRANDMEMBER letters
"a"
还可以传递count
参数来一次随机获得多个元素,根据count
的正负不同,具体表现也不同。
(1)当count
为正数时,SRANDMEMBER
会随机从集合里获得count
个不重复的元素。如果count
的值大于集合中的元素个数,则SRANDMEMBER
会返回集合中的全部元素。
(2)当count
为负数时,SRANDMEMBER
会随机从集合里获得|count|
个的元素,这些元素有可能相同。
为了示例,我们先在letters
集合中加入两个元素:
redis>SADD letters c d
(integer) 2
目前 letters 集合中共有“a”、“b”、“c”、“d”4个元素,下面使用不同的参数对SRANDMEMBER
命令进行测试:
redis>SRANDMEMBER letters 2
1) "a"
2) "c"
redis>SRANDMEMBER letters 2
1) "a"
2) "b"
redis>SRANDMEMBER letters 100
1) "b"
2) "a"
3) "c"
4) "d"
redis>SRANDMEMBER letters -2
1) "b"
2) "b"
redis>SRANDMEMBER letters -10
1) "b"
2) "b"
3) "c"
4) "c"
5) "b"
6) "a"
7) "b"
8) "d"
9) "b"
10) "b"
细心的读者可能会发现SRANDMEMBER
命令返回的数据似乎并不是非常的随机,从SRANDMEMBER letters -10
这个结果中可以很明显地看出这个问题(b元素出现的次数相对较多13),出现这种情况是由集合类型采用的存储结构(散列表)造成的。散列表使用散列函数将元素映射到不同的存储位置(桶)上以实现O(1)时间复杂度的元素查找,举个例子,当使用散列表存储元素b时,使用散列函数计算出b的散列值是0,所以将b存入编号为0的桶(bucket)中,下次要查找b时就可以用同样的散列函数再次计算b的散列值并直接到相应的桶中找到b。当两个不同的元素的散列值相同时会出现冲突,Redis使用拉链法来解决冲突,即将散列值冲突的元素以链表的形式存入同一桶中,查找元素时先找到元素对应的桶,然后再从桶中的链表中找到对应的元素。使用SRANDMEMBER
命令从集合中获得一个随机元素时,Redis首先会从所有桶中随机选择一个桶,然后再从桶中的所有元素中随机选择一个元素,所以元素所在的桶中的元素数量越少,其被随机选中的可能性就越大,如图3-19所示。
13如果你亲自跟着输入了命令可能会发现得到的结果与书中的结果并不相同,这是正常现象,见后文描述。
图3-19 Redis会先从3个桶中随机挑一个非空的桶,然后再从桶中随机选择
一个元素,所以选中元素b的概率会大一些
SPOP key
3.4节中我们学习过LPOP
命令,作用是从列表左边弹出一个元素(即返回元素的值并删除它)。SPOP
命令的作用与之类似,但由于集合类型的元素是无序的,所以SPOP
命令会从集合中随机选择一个元素弹出。例如:
redis>SPOP letters
"b"
redis> SMEMBERS letters
1) "a"
2) "c"
3) "d"
了解了集合类型后,小白终于被Redis的强大功能所折服了,但他却不愿止步于此。这不,小白又想给博客加上按照文章访问量排序的功能:
老师您好,之前您已经介绍过了如何使用列表类型键存储文章ID列表,不过我还想加上按照文章访问量排序的功能,因为我觉得很多访客更希望看那些热门的文章。
宋老师回答到:
这个功能很好实现,不过要用到一个新的数据类型,也是我要介绍的最后一个数据类型—有序集合。
有序集合类型(sorted set)的特点从它的名字中就可以猜到,它与上一节介绍的集合类型的区别就是“有序”二字。
在集合类型的基础上有序集合类型为集合中的每个元素都关联了一个分数,这使得我们不仅可以完成插入、删除和判断元素是否存在等集合类型支持的操作,还能够获得分数最高(或最低)的前N个元素、获得指定分数范围内的元素等与分数有关的操作。虽然集合中每个元素都是不同的,但是它们的分数却可以相同。
有序集合类型在某些方面和列表类型有些相似。
(1)二者都是有序的。
(2)二者都可以获得某一范围的元素。
但是二者有着很大的区别,这使得它们的应用场景也是不同的。
(1)列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。
(2)有序集合类型是使用散列表和跳表(skip list)实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度是o(log(n)))。
(3)列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)。
(4)有序集合要比列表类型更耗费内存。
有序集合类型算得上是Redis的5种数据类型中最高级的类型了,在学习时可以与列表类型和集合类型对照理解。
ZADD key score member [score member …]
ZADD
命令用来向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。ZADD
命令的返回值是新加入到集合中的元素个数(不包含之前已经存在的元素)。
假设我们用有序集合模拟计分板,现在要记录Tom、Peter和David三名运动员的分数(分别是89分、67分和100分):
redis>ZADD scoreboard 89 Tom 67 Peter 100 David
(integer) 3
这时我们发现Peter的分数录入有误,实际的分数应该是76分,可以用ZADD
命令修改Peter的分数:
redis>ZADD scoreboard 76 Peter
(integer) 0
分数不仅可以是整数,还支持双精度浮点数:
redis>ZADD testboard 17E+307 a
(integer) 1
redis>ZADD testboard 1.5 b
(integer) 1
redis>ZADD testboard +inf c
(integer) 1
redis>ZADD testboard -inf d
(integer) 1
其中+inf
和-inf
分别表示正无穷和负无穷。
ZSCORE key member
示例如下:
redis>ZSCORE scoreboard Tom
"89"
ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]
ZRANGE
命令会按照元素分数从小到大的顺序返回索引从start
到stop
之间的所有元素(包含两端的元素)。ZRANGE
命令与LRANGE
命令十分相似,如索引都是从0开始,负数代表从后向前查找(−1表示最后一个元素)。就像这样:
redis>ZRANGE scoreboard 0 2
1) "Peter"
2) "Tom"
3) "David"
redis>ZRANGE scoreboard 1 -1
1) "Tom"
2) "David"
如果需要同时获得元素的分数的话可以在ZRANGE
命令的尾部加上WITHSCORES
参数,这时返回的数据格式就从“元素1, 元素2, …, 元素n”变为了“元素1, 分数1, 元素2, 分数2, …, 元素n, 分数n”,例如:
redis>ZRANGE scoreboard 0 -1 WITHSCORES
1) "Peter"
2) "76"
3) "Tom"
4) "89"
5) "David"
6) "100"
ZRANGE
命令的时间复杂度为O(log n+m)(其中n为有序集合的基数,m为返回的元素个数)。
如果两个元素的分数相同,Redis会按照字典顺序(即"0" < "9" < "A" < "Z" < "a" < "z"这样的顺序)来进行排列。再进一步,如果元素的值是中文怎么处理呢?答案是取决于中文的编码方式,如使用UTF-8编码:
redis>ZADD chineseName 0 马华 0 刘墉 0 司马光 0 赵哲
(integer) 4
redis>ZRANGE chineseName 0 -1
1) "\xe5\x88\x98\xe5\xa2\x89"
2) "\xe5\x8f\xb8\xe9\xa9\xac\xe5\x85\x89"
3) "\xe8\xb5\xb5\xe5\x93\xb2"
4) "\xe9\xa9\xac\xe5\x8d\x8e"
可见此时Redis依然按照字典顺序排列这些元素。
ZREVRANGE
命令和ZRANGE
的唯一不同在于ZREVRANGE
命令是按照元素分数从大到小的顺序给出结果的。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZRANGEBYSCORE
命令参数虽然多,但是都很好理解。该命令按照元素分数从小到大的顺序返回分数在min
和max
之间(包含min
和max
)的元素:
redis>ZRANGEBYSCORE scoreboard 80 100
1) "Tom"
2) "David"
如果希望分数范围不包含端点值,可以在分数前加上“(”符号。例如,希望返回”80分到100分的数据,可以含80分,但不包含100分,则稍微修改一下上面的命令即可:
redis>ZRANGEBYSCORE scoreboard 80 (100
1) "Tom"
min
和max
还支持无穷大,同ZADD
命令一样,-inf
和+inf
分别表示负无穷和正无穷。比如你希望得到所有分数高于80分(不包含80分)的人的名单,但你却不知道最高分是多少(虽然有些背离现实,但是为了叙述方便,这里假设可以获得的分数是无上限的),这时就可以用上+inf
了:
redis>ZRANGEBYSCORE scoreboard (80 +inf
1) "Tom"
2) "David"
WITHSCORES
参数的用法与ZRANGE
命令一样,不再赘述。
了解SQL语句的读者对LIMIT offset count
应该很熟悉,在本命令中LIMIT offset count
与 SQL 中的用法基本相同,即在获得的元素列表的基础上向后偏移offset
个元素,并且只获取前count
个元素。为了便于演示,我们先向scoreboard
键中再增加些元素:
redis>ZADD scoreboard 56 Jerry 92 Wendy 67 Yvonne
(integer) 3
现在scoreboard
键中的所有元素为:
redis>ZRANGE scoreboard 0 -1 WITHSCORES
1) "Jerry"
2) "56"
3) "Yvonne"
4) "67"
5) "Peter"
6) "76"
7) "Tom"
8) "89"
9) "Wendy"
10) "92"
11) "David"
12) "100"
想获得分数高于60分的从第二个人开始的3个人:
redis>ZRANGEBYSCORE scoreboard 60 +inf LIMIT 1 3
1) "Peter"
2) "Tom"
3) "Wendy"
那么,如果想获取分数低于或等于 100 分的前 3 个人怎么办呢?这时可以借助ZREVRANGEBYSCORE
命令实现。对照前文提到的ZRANGE
命令和ZREVRANGE
命令之间的关系,相信读者很容易能明白 ZREVRANGEBYSCORE
命令的功能。需要注意的是ZREVRANGEBYSCORE
命令不仅是按照元素分数从大往小的顺序给出结果的,而且它的min
和max
参数的顺序和ZRANGEBYSCORE
命令是相反的。就像这样:
redis>ZREVRANGEBYSCORE scoreboard 100 0 LIMIT 0 3
1) "David"
2) "Wendy"
3) "Tom"
ZINCRBY key increment member
ZINCRBY
命令可以增加一个元素的分数,返回值是更改后的分数。例如,想给Jerry加4分:
redis>ZINCRBY scoreboard 4 Jerry
"60"
increment
也可以是个负数表示减分,例如,给Jerry减4分:
redis>ZINCRBY scoreboard -4 Jerry
"56"
如果指定的元素不存在,Redis 在执行命令前会先建立它并将它的分数赋为 0 再执行操作。
要按照文章的点击量排序,就必须再额外使用一个有序集合类型的键来实现。在这个键中以文章的 ID 作为元素,以该文章的点击量作为该元素的分数。将该键命名为posts:page.view
,每次用户访问一篇文章时,博客程序就通过ZINCRBY posts:page. view 1文章ID
更新访问量。
需要按照点击量的顺序显示文章列表时,有序集合的用法与列表的用法大同小异:
$postsPerPage = 10
$start = ($currentPage - 1) * $postsPerPage
$end = $currentPage * $postsPerPage - 1
$postsID =ZREVRANGE posts:page.view, $start,$end
for each $id in $postsID
$postData =HGETALL post:$id
print 文章标题:$postData.title
另外3.2节介绍过使用字符串类型键post:
文章ID:page.view
来记录单个文章的访问量,现在这个键已经不需要了,想要获得某篇文章的访问量可以通过ZSCORE posts:page. view
文章ID来实现。
3.4节介绍了每次发布新文章时都将文章的ID加入到名为posts:list
的列表类型键中来获得按照时间顺序排列的文章列表,但是由于列表类型更改元素的顺序比较麻烦,而如今不少博客系统都支持更改文章的发布时间,为了让小白的博客同样支持该功能,我们需要一个新的方案来实现按照时间顺序排列文章的功能。
为了能够自由地更改文章发布时间,可以采用有序集合类型代替列表类型。自然地,元素仍然是文章的ID,而此时元素的分数则是文章发布的Unix时间14。通过修改元素对应的分数就可以达到更改时间的目的。
14 Unix时间指UTC时间1970年1月1日0时0分0秒起至现在的总秒数(不包括闰秒)。为什么是1970年呢?因为Unix在1970年左右诞生。
另外借助ZREVRANGEBYSCORE
命令还可以轻松获得指定时间范围的文章列表,借助这个功能可以实现类似WordPress的按月份查看文章的功能。
ZCARD key
例如:
redis>ZCARD scoreboard
(integer) 6
ZCOUNT key min max
例如:
redis>ZCOUNT scoreboard 90 100
(integer) 2
ZCOUNT
命令的min
和max
参数的特性与ZRANGEBYSCORE
命令中的一样:
redis>ZCOUNT scoreboard (89 +inf
(integer) 2
ZREM key member [member …]
ZREM
命令的返回值是成功删除的元素数量(不包含本来就不存在的元素)。
redis>ZREM scoreboard Wendy
(integer) 1
redis>ZCARD scoreboard
(integer) 5
ZREMRANGEBYRANK key start stop
ZREMRANGEBYRANK
命令按照元素分数从小到大的顺序(即索引0表示最小的值)删除处在指定排名范围内的所有元素,并返回删除的元素数量。如:
redis>ZADD testRem 1 a 2 b 3 c 4 d 5 e 6 f
(integer) 6
redis>ZREMRANGEBYRANK testRem 0 2
(integer) 3
redis>ZRANGE testRem 0 -1
1) "d"
2) "e"
3) "f"
ZREMRANGEBYSCORE key min max
ZREMRANGEBYSCORE
命令会删除指定分数范围内的所有元素,参数min
和max
的特性和ZRANGEBYSCORE
命令中的一样。返回值是删除的元素数量。如:
redis>ZREMRANGEBYSCORE testRem (4 5
(integer) 1
redis>ZRANGE testRem 0 -1
1) "d"
2) "f"
ZRANK key member
ZREVRANK key member
ZRANK
命令会按照元素分数从小到大的顺序获得指定的元素的排名(从0开始,即分数最小的元素排名为0)。如:
redis>ZRANK scoreboard Peter
(integer) 0
ZREVRANK
命令则相反(分数最大的元素排名为0):
redis>ZREVRANK scoreboard Peter
(integer) 4
ZINTERSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]]
[AGGREGATE SUM|MIN|MAX]
ZINTERSTORE
命令用来计算多个有序集合的交集并将结果存储在destination
键中(同样以有序集合类型存储),返回值为destination
键中的元素个数。
destination
键中元素的分数是由AGGREGATE
参数决定的。
(1)当AGGREGATE
是SUM
时(也就是默认值),destination
键中元素的分数是每个参与计算的集合中该元素分数的和。例如:
redis> ZADD sortedSets1 1 a 2 b
(integer) 2
redis> ZADD sortedSets2 10 a 20 b
(integer) 2
redis> ZINTERSTORE sortedSetsResult 2 sortedSets1 sortedSets2
(integer) 2
redis> ZRANGE sortedSetsResult 0 -1 WITHSCORES
1) "a"
2) "11"
3) "b"
4) "22"
(2)当AGGREGATE
是MIN
时,destination
键中元素的分数是每个参与计算的集合中该元素分数的最小值。例如:
redis> ZINTERSTORE sortedSetsResult 2 sortedSets1 sortedSets2 AGGREGATE MIN
(integer) 2
redis> ZRANGE sortedSetsResult 0 -1 WITHSCORES
1) "a"
2) "1"
3) "b"
4) "2"
(3)当AGGREGATE
是MAX
时,destination
键中元素的分数是每个参与计算的集合中该元素分数的最大值。例如:
redis> ZINTERSTORE sortedSetsResult 2 sortedSets1 sortedSets2 AGGREGATE MAX
(integer) 2
redis> ZRANGE sortedSetsResult 0 -1 WITHSCORES
1) "a"
2) "10"
3) "b"
4) "20"
ZINTERSTORE
命令还能够通过WEIGHTS
参数设置每个集合的权重,每个集合在参与计算时元素的分数会被乘上该集合的权重。例如:
redis> ZINTERSTORE sortedSetsResult 2 sortedSets1 sortedSets2 WEIGHTS 1 0.1
(integer) 2
redis> ZRANGE sortedSetsResult 0 -1 WITHSCORES
1) "a"
2) "2"
3) "b"
4) "4"
另外还有一个命令与ZINTERSTORE
命令的用法一样,名为ZUNIONSTORE
,它的作用是计算集合间的并集,这里不再赘述。