Redis基础教程:从入门到精通

发表时间: 2020-09-14 17:41

学会如何安装和运行Redis,并了解Redis的基础知识后,本章将详细介绍Redis的5种主要数据类型及相应的命令,带领读者真正进入Redis的世界。在学习的时候,手边打开一个redis-cli程序来跟着一起输入命令将会极大地提高学习效率。尽管在目前多数公司和团队的Redis的应用是以缓存和队列为主。

在之后的章节中你会遇到两个学习伙伴:小白和宋老师。小白是一个标准的极客,最近刚开始他的Redis学习之旅,而他大学时的计算机老师宋老师恰好对Redis颇有研究,于是就顺理成章地成为了小白的私人Redis教师。这不,小白想基于Redis开发一个博客,于是找到宋老师,向他请教。在本章中宋老师会向小白介绍Redis最核心的内容—数据类型,从他们的对话中你一定能学到不少知识!

3.2节到3.6节这5节将分别介绍Redis的5种数据类型,其中每节都是由4个部分组成,依次是“介绍”、“命令”、“实践”和“命令拾遗”。“介绍”部分是对数据类型的概述,“命令”部分会对“实践”部分将用到的命令进行介绍,“实践”部分会讲解该数据类型在开发中的应用方法,“命令拾遗”部分会对该数据类型其他比较有用的命令进行补充介绍。

3.1 热身

在介绍Redis的数据类型之前,我们先来了解几个比较基础的命令作为热身,赶快打开redis-cli,跟着样例亲自输入命令来体验一下吧!

1.获得符合规则的键名列表

KEYS pattern

pattern支持glob风格通配符格式,具体规则如表3-1所示。

表3-1 glob风格通配符规则

符 号含  义
?  匹配一个字符
*  匹配任意个(包括0个)字符
[]  匹配括号间的任一字符,可以使用“-”符号表示一个范围,如a[b-d]可以匹配“ab”、“ac”和“ad”
\x  匹配字符x,用于转义符号。如要匹配“?”就需要使用\?

现在Redis中空空如也(如果你从第2章开始就一直跟着本书的进度输入命令,此时数据库中可能还会有个foo键),为了演示KEYS命令,首先我们得给Redis加点料。使用SET命令(会在3.2节介绍)建立一个名为bar的键:

redis> SET bar 1OK

然后使用KEYS *就能获得Redis中所有的键了(当然由于数据库中只有一个bar键,所以KEYS ba*或者KEYS bar等命令都能获得同样的结果):

redis> KEYS *1) "bar"

注意

KEYS命令需要遍历Redis中的所有键,当键的数量较多时会影响性能,不建议在生产环境中使用。

提示

Redis不区分命令大小写,但在本书中均会使用大写字母表示Redis命令。

2.判断一个键是否存在

EXISTS key

如果键存在则返回整数类型1,否则返回0。例如:

redis> EXISTS bar(integer) 1redis> EXISTS noexists(integer) 0

3.删除键

DEL key [key …]

可以删除一个或多个键,返回值是删除的键的个数。例如:

redis> DEL bar(integer) 1redis> 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:*" 来达到同样的效果,但是性能更好。

4.获得键值的数据类型

TYPE key

TYPE命令用来获得键值的数据类型,返回值可能是string(字符串类型)、hash(散列类型)、list(列表类型)、set(集合类型)、zset(有序集合类型)。例如:

redis> SET foo 1OKredis> TYPE foostringredis> LPUSH bar 1(integer) 1redis> TYPE barlist

LPUSH命令的作用是向指定的列表类型键中增加一个元素,如果键不存在则创建它,3.4节会详细介绍。

3.2 字符串类型

作为一个爱造轮子的资深极客,小白每次看到自己博客最下面的“Powered by WordPress”{![即“由WordPress驱动”。WordPress是一个开源的博客程序,用户可以借其通过简单的配置搭建一个博客或内容管理系统。]}都觉得有些不舒服,终于有一天他下定决心要开发一个属于自己的博客。但是用腻了MySQL数据库的小白总想尝试一下新技术,恰好上次参加Node Party时听人介绍过Redis数据库,便想着趁机试一试。可小白只知道Redis是一个键值对数据库,其他的一概不知。抱着试一试的态度,小白找到了自己大学时教计算机的宋老师,一问之下欣喜地发现宋老师竟然对Redis颇有研究。宋老师有感于小白的好学,决定给小白开个小灶。

小白:

宋老师您好,我最近听别人介绍过Redis,当时就对它很感兴趣。恰好最近想开发一个博客,准备尝试一下它。有什么能快速学会Redis的方法吗?

宋老师笑着说:

心急吃不了热豆腐,要学会Redis就要先掌握Redis的键值数据类型和相关的命令,这些内容是Redis的基础。为了让你更全面地了解Redis的每种数据类型,接下来我会先讲解如何将Redis作为数据库使用,但是实际上Redis可不只是数据库这么简单,更多的公司和团队将Redis用作缓存和队列系统,而这部分内容等你掌握了Redis的基础后我会再进行介绍。作为开始,我先来讲讲Redis中最基本的数据类型—字符串类型。

3.2.1 介绍

字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。你可以用其存储用户的邮箱、JSON 化的对象甚至是一张图片。一个字符串类型键允许存储的数据的最大容量是512 MB{![ Redis的作者考虑过让字符串类型键支持超过 512 MB大小的数据,未来的版本也可能会放宽这一限制,但无论如何,考虑到Redis的数据是使用内存存储的,512 MB的限制已经非常宽松了。]}。

字符串类型是其他4种数据类型的基础,其他数据类型和字符串类型的差别从某种角度来说只是组织字符串的形式不同。例如,列表类型是以列表的形式组织字符串,而集合类型是以集合的形式组织字符串。学习过本章后面几节后相信读者对此会有更深的理解。

3.2.2 命令

1.赋值与取值

SET key valueGET key

SET和GET是Redis中最简单的两个命令,它们实现的功能和编程语言中的读写变量相似,如key = "hello"在Redis中是这样表示的:

redis> SET key helloOK

想要读取键值则更简单:

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-2{441}

图3-1 设置过姓名时的页面


3-1{440}

              图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(键名)。

2.递增数字

INCR key前面说过字符串类型可以存储任何形式的字符串,当存储的字符串是整数形式时,Redis提供了一个实用的命令`INCR`,其作用是让当前键值递增,并返回递增后的值,用法为:redis> INCR num(integer) 1redis> INCR num(integer) 2当要操作的键不存在时会默认键值为0,所以第一次递增后的结果是1。当键值不是整数时Redis会提示错误:redis> SET foo loremOKredis> 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){![竞态条件是指一个系统或者进程的输出,依赖于不受控制的事件的出现顺序或者出现时机。]}。例如有两个客户端A和B都要执行我们自己实现的incr函数并准备将同一个键的键值递增,当它们恰好同时执行到代码第二行时二者读取到的键值是一样的,如“5”,而后它们各自将该值递增到“6”并使用SET命令将其赋给原键,结果虽然对键执行了两次递增操作,最终的键值却是“6”而不是预想中的“7”。包括INCR在内的所有Redis命令都是原子操作(atomic operation){![原子操作取“原子”的“不可拆分”的意思,原子操作是最小的执行单位,不会在执行的过程中被其他命令插入打断。]},无论多少个客户端同时连接,都不会出现上述情况。之后我们还会介绍利用事务(4.1节)和脚本(第6章)实现自定义的原子操作的方法。

3.2.3 实践

1.文章访问量统计

博客的一个常见的功能是统计文章的访问量,我们可以为每篇文章使用一个名为post:文章ID:page.view的键来记录文章的访问量,每次访问文章的时候使用INCR命令使相应的键值递增。

提示

Redis对于键的命名并没有强制的要求,但比较好的实践是用“对象类型:对象ID:对象属性”来命名一个键,如使用键user:1:friends来存储ID为1的用户的好友列表。对于多个单词则推荐使用“.”分隔,一方面是沿用以前的习惯(Redis以前版本的键名不能包含空格等特殊字符),另一方面是在redis-cli中容易输入,无需使用双引号包裹。另外为了日后维护方便,键的命名一定要有意义,如u:1:f的可读性显然不如user:1:friends好(虽然采用较短的名称可以节省存储空间,但由于键值的长度往往远远大于键名的长度,所以这部分的节省大部分情况下并不如可读性来得重要)。

2.生成自增ID

那么怎么为每篇文章生成一个唯一ID呢?在关系数据库中我们通过设置字段属性为AUTO_INCREMENT来实现每增加一条记录自动为其生成一个唯一的递增ID的目的,而在Redis中可以通过另一种模式来实现:对于每一类对象使用名为对象类型(复数形式):count{![这个键名只是参考命名,实际应用中可以使用任何容易理解的名称。]}的键(如users:count)来存储当前类型对象的数量,每增加一个新对象时都使用INCR命令递增该键的值。由于使用INCR命令建立的键的初始键值是1,所以可以很容易得知,INCR命令的返回值既是加入该对象后的当前类型的对象总数,又是该新增对象的ID。

3.存储文章数据

由于每个字符串类型键只能存储一个字符串,而一篇博客文章是由标题、正文、作者与发布时间等多个元素构成的。为了存储这些元素,我们需要使用序列化函数(如PHP中的serialize和JavaScript中的JSON.stringify)将它们转换成一个字符串。除此之外因为字符串类型键可以存储二进制数据,所以也可以使用MessagePack进行序列化,速度更快,占用空间也更小。

至此我们已经可以写出发布新文章时与Redis操作相关的伪代码首先获得新文章的ID

$postID = INCR posts:countalert("Hello CSDN");将博客文章的诸多元素序列化成字符串$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节讨论。

3.2.4 命令拾遗

1.增加指定的整数

INCRBY key increment

alert("Hello CSDN");

`INCRBY`命令与`INCR`命令基本一样,只不过前者可以通过`increment`参数指定一次增加的数值,如:redis> INCRBY bar 2(integer) 2redis> INCRBY bar 3(integer) 5alert("Hello CSDN");

2.减少指定的整数

DECR key
DECRBY key decrement

DECR命令与INCR命令用法相同,只不过是让键值递减,例如:

redis> DECR bar(integer) 4

而DECRBY命令的作用不用介绍想必读者就可以猜到,DECRBY key 5相当于INCRBY key –5。

3.增加指定浮点数

INCRBYFLOAT key increment

INCRBYFLOAT命令类似INCRBY命令,差别是前者可以递增一个双精度浮点数,如:

redis> INCRBYFLOAT bar 2.7"6.7"redis> INCRBYFLOAT bar 5E+4"50006.69999999999999929"

4.向尾部追加值

APPEND key value

APPEND作用是向键值的末尾追加value。如果键不存在则将该键的值设置为value,即相当于SET key value。返回值是追加后字符串的总长度。如:

redis> SET key helloOKredis> APPEND key " world!"(integer) 12

此时key的值是"hello world!"。APPEND命令的第二个参数加了双引号,原因是该参数包含空格,在redis-cli中输入需要双引号以示区分。

5.获取字符串长度

STRLEN key

STRLEN命令返回键值的长度,如果键不存在则返回0。例如:

redis> STRLEN key(integer) 12redis> SET key 你好OKredis> STRLEN key(integer) 6

前面提到了字符串类型可以存储二进制数据,所以它可以存储任何编码的字符串。例子中Redis接收到的是使用UTF-8编码的中文,由于“你”和“好”两个字的UTF-8编码的长度都是3,所以此例中会返回6。

6.同时获得/设置多个键值

MGET key [key …]
MSET key value [key value …]

MGET/MSET与GET/SET相似,不过MGET/MSET可以同时获得/设置多个键的键值。例如:

redis> MSET key1 v1 key2 v2 key3 v3OKredis> GET key2"v2"redis> MGET key1 key31) "v1"2) "v3"

7.位操作

GETBIT key offset
SETBIT key offset value
BITCOUNT key [start] [end]
BITOP operation destkey key [key …]

一个字节由8个二进制位组成,Redis提供了4个命令可以直接对二进制位进行操作。为了演示,我们首先将foo键赋值为bar:

redis> SET foo barOK

bar的3个字母“b”“a”和“r”对应的ASCII码分别为98、97和114,转换成二进制后分别为1100010、1100001和1110010,所以foo键中的二进制位结构如图3-3所示。


3-3{513}

图3-3 bar的二进制存储结构

GETBIT命令可以获得一个字符串类型键指定位置的二进制位的值(0或1),索引从0开始:

redis> GETBIT foo 0(integer) 0redis> 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) 1redis> SETBIT foo 7 1(integer) 0redis> GET foo"aar"

如果要设置的位置超过了键值的二进制位的长度,SETBIT命令会自动将中间的二进制位设置为0,同理设置一个不存在的键的指定二进制位的值会自动将其前面的位赋值为0:

redis> SETBIT nofoo 10 1(integer) 0redis> 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 barOKredis> SET foo2 aarOKredis> BITOP OR res foo1 foo2(integer) 3redis> GET res"car"

运算过程如图3-4所示。


3-4

{-:-}图3-4 OR运算过程示意

Redis 2.8.7引入了BITPOS命令,可以获得指定键的第一个位值是0或者1的位置。还是以“bar”这个键值为例,如果想获取键值中的第一个二进制位为1的偏移量,则可以执行:

redis> SET foo barOKredis> BITPOS foo 1(integer) 1

结合图3-3可以看出,正如BITPOS命令的结果所示,“bar”中的第一个值为1的二进制位的偏移量为1(同其他命令一样,BITPOS命令的索引也是从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再进行存储。

3.3 散列类型

小白只用了半个多小时就把访问统计和发表文章两个部分做好了。同时借助Bootstrap框架,老师花了一小会儿时间教会了之前只涉猎过HTML的小白如何做出一个像样的网页界面。

接着小白发问:

接下来我想要做的功能是博客的文章列表页,我设想在列表页中每个文章只显示标题部分,可是使用您刚才介绍的方法,若想取得文章的标题,必须把整个文章数据字符串取出来反序列化,而其中占用空间最大的文章内容部分却是不需要的,这样难道不会在传输和处理时造成资源浪费吗?

老师有些惊喜地看着小白答道:“很对!”同时以一个夸张的幅度点了下头,接着说:

这正是我接下来准备讲的。不仅取数据时会有资源浪费,在修改数据时也会有这个问题,比如当你只想更改文章的标题时也不得不把整个文章数据字符串更新一遍。

没等小白再问,老师就又继续说道:

前面我说过Redis的强大特性之一就是提供了多种实用的数据类型,其中的散列类型可以非常好地解决这个问题。

3.3.1 介绍

我们现在已经知道Redis是采用字典结构以键值对的形式存储数据的,而散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他数据类型,换句话说,散列类型不能嵌套其他的数据类型。一个散列类型键可以包含至多232−1个字段。

提示

除了散列类型,Redis 的其他数据类型同样不支持数据类型嵌套。比如集合类型的每个元素都只能是字符串,不能是另一个集合或散列表等。

散列类型适合存储对象:使用对象类别和ID构成键名,使用字段表示对象的属性,而字段值则存储属性值。例如要存储ID为2的汽车对象,可以分别使用名为color、name和price的3个字段来存储该辆汽车的颜色、名称和价格。存储结构如图3-5所示。


3-5{480}

图3-5 使用散列类型存储汽车对象的结构图

回想在关系数据库中如果要存储汽车对象,存储结构如表3-2所示。

表3-2 关系数据库存储汽车资料的表结构

IDcolornameprice
1黑色宝马100万
2白色奥迪90万
3蓝色宾利600万

数据是以二维表的形式存储的,这就要求所有的记录都拥有同样的属性,无法单独为某条记录增减属性。如果想为ID为1的汽车增加生产日期属性,就需要把数据表更改为如表3-3所示的结构。

表3-3 为其中一辆汽车增加一个“属性”

IDcolornamepricedate
1黑色宝马100万2012年12月21日
2白色奥迪90万
3蓝色宾利600万

对于ID为2和3的两条记录而言date字段是冗余的。可想而知当不同的记录需要不同的属性时,表的字段数量会越来越多以至于难以维护。而且当使用ORM{![即Object-Relational Mapping(对象关系映射)。]}将关系数据库中的对象实体映射成程序中的实体时,修改表的结构往往意味着要中断服务(重启网站程序)。为了防止这些问题,在关系数据库中存储这种半结构化数据还需要额外的表才行。

而Redis的散列类型则不存在这个问题。虽然我们在图3-5中描述了汽车对象的存储结构,但是这个结构只是人为的约定,Redis并不要求每个键都依据此结构存储,我们完全可以自由地为任何键增减字段而不影响其他键。

3.3.2 命令

1.赋值与取值

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) 1redis> HSET car name BMW(integer) 1redis> 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"{![并不是所有命令都是如此,比如SET命令可以覆盖已经存在的键而不论原来键是什么类型。]}。

当需要同时设置多个字段的值时,可以使用HMSET命令。例如,下面两条语句

HSET key field1 value1HSET key field2 value2

可以用HMSET命令改写成

HMSET key field1 value1 field2 value2

相应地,HMGET命令可以同时获得多个字段的值:

redis> HMGET car price name1) "500"2) "BMW"

如果想获取键中所有字段和字段值却不知道键中有哪些字段时(如3.3.1节介绍的存储汽车对象的例子,每个对象拥有的属性都未必相同)应该使用HGETALL命令。如:

redis> HGETALL car1) "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);});

2.判断字段是否存在

HEXISTS key field

HEXISTS命令用来判断一个字段是否存在。如果存在则返回1,否则返回0(如果键不存在也会返回0)。

redis> HEXISTS car model(integer) 0redis> HSET car model C200(integer) 1redis> HEXISTS car model(integer) 1

3.当字段不存在时赋值

HSETNX key field value

HSETNX{![HSETNX中的“NX”表示“if Not eXists”(如果不存在)。]}命令与HSET命令类似,区别在于如果字段已经存在,HSETNX命令将不执行任何操作。其实现可以表示为如下伪代码:

def hsetnx($key, $field, $value)  $isExists = HEXISTS $key, $field  if $isExists is 0    HSET $key, $field, $value    return 1  else    return 0

只不过HSETNX命令是原子操作,不用担心竞态条件。

4.增加数字

HINCRBY key field increment

上一节的命令拾遗部分介绍了字符串类型的命令INCRBY,HINCRBY命令与之类似,可以使字段值增加指定的整数。散列类型没有HINCR命令,但是可以通过HINCRBY key field 1来实现。

HINCRBY命令的示例如下:

redis> HINCRBY person score 60   (integer) 60

之前person键不存在,HINCRBY命令会自动建立该键并默认score字段在执行命令前的值为“0”。命令的返回值是增值后的字段值。

5.删除字段

HDEL key field [field …]

HDEL命令可以删除一个或多个字段,返回值是被删除的字段个数:

redis> HDEL car price(integer) 1redis> HDEL car price(integer) 0

3.3.3 实践

1.存储文章数据

3.2.3节介绍了可以将文章对象序列化后使用一个字符串类型键存储,可是这种方法无法提供对单个字段的原子读写操作支持,从而产生竞态条件,如两个客户端同时获得并反序列化某个文章的数据,然后分别修改不同的属性后存入,显然后存入的数据会覆盖之前的数据,最后只会有一个属性被修改。另外如小白所说,即使只需要文章标题,程序也不得不将包括文章内容在内的所有文章数据取出并反序列化,比较消耗资源。

除此之外,还有一种方法是组合使用多个字符串类型键来存储一篇文章的数据,如图3-6所示。


3-6{300}

{-:-}图3-6 使用多个字符串类型键存储一个对象

使用这种方法的好处在于无论获取还是修改文章数据,都可以只对某一属性进行操作,十分方便。而本章介绍的散列类型则更适合此场景,使用散列类型的存储结构如图3-7所示。

从图3-7可以看出使用散列类型存储文章数据比图3-6所示的方法看起来更加直观,也更容易维护(比如可以使用HGETALL命令获得一个对象的所有字段,删除一个对象时只需要删除一个键),另外存储同样的数据散列类型往往比字符串类型更加节约空间,具体的细节会在4.6节中介绍。

2.存储文章缩略名

使用过WordPress的读者可能会知道发布文章时一般需要指定一个缩略名(slug)来构成该篇文章的网址的一部分,缩略名必须符合网址规范且最好可以与文章标题含义相似,如“This Is A Great Post!”的缩略名可以为“this-is-a-great-post”。每个文章的缩略名必须是唯一的,所以在发布文章时程序需要验证用户输入的缩略名是否存在,同时也需要通过缩略名获得文章的ID。


3-7{504}

图3-7 使用一个散列类型键存储一个对象

我们可以使用一个散列类型的键slug.to.id来存储文章缩略名和ID之间的映射关系。其中字段用来记录缩略名,字段值用来记录缩略名对应的ID。这样就可以使用HEXISTS命令来判断缩略名是否存在,使用HGET命令来获得缩略名对应的文章ID了。

现在发布文章可以修改成如下代码:

$postID = INCR posts:count# 判断用户输入的slug是否可用,如果可用则记录$isSlugAvailable = HSETNX slug.to.id, $slug, $postIDif $isSlugAvailable is 0  # slug已经用过了,需要提示用户更换slug,  # 这里为了演示方便直接退出。  exitHMSET post:$postID, title, $title, content, $content, slug, $slug,...

这段代码使用了HSETNX命令原子地实现了HEXISTS和HSET两个命令以避免竞态条件。当用户访问文章时,我们从网址中得到文章的缩略名,并查询slug.to.id键来获取文章ID:

$postID = HGET slug.to.id, $slugif not $postID  print 文章不存在  exit$post = HGETALL post:$postIDprint 文章标题:$post.title

需要注意的是如果要修改文章的缩略名一定不能忘了修改slug.to.id键对应的字段。如要修改ID为42的文章的缩略名为newSlug变量的值:

# 判断新的slug是否可用,如果可用则记录$isSlugAvailable = HSETNX slug.to.id, $newSlug, 42if $isSlugAvailable is 0  exit# 获得旧的缩略名$oldSlug = HGET post:42, slug# 设置新的缩略名HSET post:42, slug, $newSlug# 删除旧的缩略名HDEL slug.to.id, $oldSlug

3.3.4 命令拾遗

1.只获取字段名或字段值

HKEYS keyHVALS key

有时仅仅需要获取键中所有字段的名字而不需要字段值,那么可以使用HKEYS命令,就像这样:

redis> HKEYS car1) "name"2) "model"

HVALS命令与HKEYS命令相对应,HVALS命令用来获得键中所有字段值,例如:

redis> HVALS car1) "BMW"2) "C200"

2.获得字段数量

HLEN key

例如:

redis> HLEN car(integer) 2

3.4 列表类型

正当小白踌躇满志地写着文章列表页的代码时,一个很重要的问题阻碍了他的开发,于是他请来了宋老师为他讲解。

原来小白是使用如下流程获得文章列表的:

  •  读取posts:count键获得博客中最大的文章ID;
  •  根据这个ID来计算当前列表页面中需要展示的文章ID列表(小白规定博客每页只显示10篇文章,按照ID的倒序排列),如第n页的文章ID范围是从最大的文章ID - (n - 1) * 10"到"max(最大的文章ID - n * 10 + 1, 1)";
  •  对每个ID使用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的列表类型来解决。”

3.4.1 介绍

列表类型(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个元素。

3.4.2 命令

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{478}

{-:-}图3-8 加入元素1后numbers键中的数据


3-9{454}

{-:-}图3-9 加入元素2,3后numbers键中的数据

向列表右边增加元素的话则使用RPUSH命令,其用法和LPUSH命令一样:

redis> RPUSH numbers 0 −1(integer) 5

此时numbers键中的数据如图3-10所示。


3-10{438}

图3-10 使用RPUSH命令加入元素0,-1后numbers键中的数据

2.从列表两端弹出元素

LPOP keyRPOP 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{438}

{-:-}图3-11 从左侧弹出元素后numbers键中的数据


3-12{438}

{-:-}图3-12 从右侧弹出元素后numbers键中的数据

3.获取列表中元素的个数

LLEN key

当键不存在时LLEN会返回0:

redis> LLEN numbers (integer) 3

LLEN命令的功能类似SQL语句SELECT COUNT(*) FROM table_name,但是LLEN的时间复杂度为O(1),使用时Redis会直接读取现成的值,而不需要像部分关系数据库(如使用InnoDB存储引擎的MySQL表)那样需要遍历一遍数据表来统计条目数量。

4.获得列表片段

LRANGE key start stop

LRANGE命令是列表类型最常用的命令之一,它能够获得列表中的某一片段。LRANGE命令将返回索引从start到stop之间的所有元素(包含两端的元素)。与大多数人的直觉相同,Redis的列表起始索引为0:

redis> LRANGE numbers 0 21) "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 -11) "1"2) "0"

显然,LRANGE numbers 0 -1可以获取列表中的所有元素。另外一些特殊情况如下。

1.如果start的索引位置比stop的索引位置靠后,则会返回空列表。

2.如果stop大于实际的索引范围,则会返回到列表最右边的元素:

redis> LRANGE numbers 1 9991) "1"2) "0"

5.删除列表中指定的值

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) 4redis> LRANGE numbers 0 -11) "2"2) "1"3) "0"4) "2"# 从右边开始删除第一个值为"2"的元素redis> LREM numbers -1 2(integer) 1redis> LRANGE numbers 0 -11) "2"2) "1"3) "0"

3.4.3 实践

1.存储文章ID列表

为了解决小白遇到的问题,我们使用列表类型键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){![4.5节中还会详细介绍这个概念。]},之后我们会介绍使用管道和脚本来优化这个问题。

另外使用列表类型键存储文章ID列表有以下两个问题。

(1)文章的发布时间不易修改:修改文章的发布时间不仅要修改post:文章ID中的time字段,还需要按照实际的发布时间重新排列posts:list中的元素顺序,而这一操作相对比较繁琐。

(2)当文章数量较多时访问中间的页面性能较差:前面已经介绍过,列表类型是通过链表实现的,所以当列表元素非常多时访问中间的元素效率并不高。

但如果博客不提供修改文章时间的功能并且文章数量也不多时,使用列表类型也不失为一种好办法。对于小白要做的博客系统来讲,现阶段的成果已经足够实用且值得庆祝了。3.6节将介绍使用有序集合类型存储文章ID列表的方法。

2.存储评论列表

在博客中还可以使用列表类型键存储文章的评论。由于小白的博客不允许访客修改自己发表的评论,而且考虑到读取评论时需要获得评论的全部数据(评论者姓名,联系方式,评论时间和评论内容),不像文章一样有时只需要文章标题而不需要文章正文。所以适合将一条评论的各个元素序列化成字符串后作为列表类型键中的元素来存储。

我们使用列表类型键post:文章ID:comments来存储某个文章的所有评论。发布评论的伪代码如下(以ID为42的文章为例):

# 将评论序列化成字符串$serializedComment = serialize($author, $email, $time, $content)LPUSH post:42:comments, $serializedComment

读取评论时同样使用LRANGE命令即可,具体的实现在此不再赘述。

3.4.4 命令拾遗

1.获得/设置指定索引的元素值

LINDEX key indexLSET 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 7OKredis> LINDEX numbers 1"7"

2.只保留列表指定片段

LTRIM key start end

LTRIM命令可以删除指定索引范围之外的所有元素,其指定列表范围的方法和LRANGE命令相同。就像这样:

redis> LRANGE numbers 0 -11) "1"2) "2"3) "7"4) "3""0"redis> LTRIM numbers 1 2OKredis> LRANGE numbers 0 11) "2"2) "7"

LTRIM命令常和LPUSH命令一起使用来限制列表中元素的数量,比如记录日志时我们希望只保留最近的100条日志,则每次加入新元素时调用一次LTRIM命令即可:

LPUSH logs $newLogLTRIM logs 0 99

3.向列表中插入元素

LINSERT key BEFORE|AFTER pivot value

LINSERT命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。

LINSERT命令的返回值是插入后列表的元素个数。示例如下:

redis> LRANGE numbers 0 -11) "2"2) "7"3) "0"redis> LINSERT numbers AFTER 7 3(integer) 4redis> LRANGE numbers 0 -11) "2"2) "7"3) "3"4) "0"redis> LINSERT numbers BEFORE 2 1(integer) 5redis> LRANGE numbers 0 -11) "1"2) "2"3) "7"4) "3"5) "0"

4.将元素从一个列表转到另一个列表

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命令的好处在于在程序执行过程中仍然可以不断地向网址列表中加入新网址,而且整个系统容易扩展,允许多个客户端同时处理队列。

3.5 集合类型

博客首页,文章页面,评论页面……眼看着博客逐渐成型,小白的心情也是越来越好。时间已经到了深夜,小白却还陶醉于编码之中。不过一个他无法解决的问题最终还是让他不得不提早睡觉去:小白不知道该怎么在Redis中存储文章标签(tag)。他想过使用散列类型或列表类型存储,虽然都能实现,但是总觉得颇有不妥,再加上之前几天领略了Redis的强大功能后,小白相信一定有一种合适的数据类型能满足他的需求。于是小白给宋老师发了封询问邮件后就睡觉去了。

转天一早就收到了宋老师的回复:

你很善于思考嘛!你想的没错,Redis 有一种数据类型很适合存储文章的标签,它就是集合类型。

3.5.1 介绍

集合的概念高中的数学课就学习过。在集合中的每个元素都是不同的,且没有顺序。一个集合类型(set)键可以存储至多232 −1个(相信这个数字对大家来说已经很熟悉了)字符串。

集合类型和列表类型有相似之处,但很容易将它们区分开来,如表3-4所示。

表3-4 集合类型和列表类型对比

集 合 类 型列 表 类 型
存储内容至多232 −1个字符串至多232 − 1个字符串
有序性否是
唯一性是否

集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算,稍后就会看到灵活运用这一特性带来的便利。

3.5.2 命令

1.增加/删除元素

SADD key member [member …]SREM key member [member …]

SADD命令用来向集合中增加一个或多个元素,如果键不存在则会自动创建。因为在一个集合中不能有相同的元素,所以如果要加入的元素已经存在于集合中就会忽略这个元素。本命令的返回值是成功加入的元素数量(忽略的元素不计算在内)。例如:

redis> SADD letters a  (integer) 1redis> SADD letters a b c(integer) 2

第二条SADD命令的返回值为2是因为元素“a”已经存在,所以实际上只加入了两个元素。

SREM命令用来从集合中删除一个或多个元素,并返回删除成功的个数,例如:

redis> SREM letters c d (integer) 1

由于元素“d”在集合中不存在,所以只删除了一个元素,返回值为1。

2.获得集合中的所有元素

SMEMBERS key

SMEMBERS命令会返回集合中的所有元素,例如:

redis> SMEMBERS letters1) "b"2) "a"

3.判断元素是否在集合中

SISMEMBER key member

判断一个元素是否在集合中是一个时间复杂度为O(1)的操作,无论集合中有多少个元素,SISMEMBER命令始终可以极快地返回结果。当值存在时SISMEMBER命令返回1,当值不存在或键不存在时返回0,例如:

redis> SISMEMBER letters a(integer) 1redis> SISMEMBER letters d(integer) 0

4.集合间运算

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} = 原子操作{2, 3, 4} - {1, 2, 3} = {4}

SDIFF命令的使用方法如下:

redis> SADD setA 1 2 3(integer) 3redis> SADD setB 2 3 4(integer) 3redis> SDIFF setA setB1) "1"redis> SDIFF setB setA1) "4"

SDIFF命令支持同时传入多个键,例如:

redis> SADD setC 2 3(integer) 2redis> SDIFF setA setB setC1) "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 setB1) "2"2) "3"

SINTER命令同样支持同时传入多个键,如:

redis> SINTER setA setB setC1) "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{200}

图3-14 图中斜线部分表示A ∩ B


3-15{200}

图3-15 图中斜线部分表示A ∪ B

SUNION命令的使用方法如下:

redis> SUNION setA setB1) "1"2) "2"3) "3"4) "4"

SUNION命令同样支持同时传入多个键,例如:

redis> SUNION setA setB setC1) "1"2) "2"3) "3"4) "4"

3.5.3 实践

1.存储文章标签

考虑到一个文章的所有标签都是互不相同的,而且展示时对这些标签的排列顺序并没有要求,我们可以使用集合类型键存储文章标签。

对每篇文章使用键名为post:文章ID:tags的键存储该篇文章的标签。具体操作如伪代码:

# 给ID为42的文章增加标签:SADD post:42:tags, 闲言碎语, 技术文章, Java# 删除标签:SREM post:42:tags, 闲言碎语# 显示所有的标签:$tags = SMEMBERS post:42:tagsprint $tags

使用集合类型键存储标签适合需要单独增加或删除标签的场合。如在WordPress博客程序中无论是添加还是删除标签都是针对单个标签的(如图3-16所示),可以直观地使用SADD和SREM命令完成操作。

另一方面,有些地方需要用户直接设置所有标签后一起上传修改,图3-17所示是某网站的个人资料编辑页面,用户编辑自己的爱好后提交,程序直接覆盖原来的标签数据,整个过程没有针对单个标签的操作,并未利用到集合类型的优势,所以此时也可以直接使用字符串类型键存储标签数据。


3-16{384}

{-:-}图3-16 在WordPress中设置文章标签


3-17{534}

{-:-}图3-17 在百度中设置个人爱好

之所以特意提到这个在实践中的差别是想说明对于Redis存储方式的选择并没有绝对的规则,比如3.4节介绍过使用列表类型存储访客评论,但是在一些特定的场合下散列类型甚至字符串类型可能更适合。

2.通过标签搜索文章

有时我们还需要列出某个标签下的所有文章,甚至需要获得同时属于某几个标签的文章列表,这种需求在传统关系数据库中实现起来比较复杂,下面举一个例子。

现有3张表,即posts、tags和posts_tags,分别存储文章数据、标签、文章与标签的对应关系。结构分别如表3-5、表3-6、表3-7所示。

表3-5 posts表结构

字 段 名说  明
post_id文章ID
post_title文章标题

表3-6 tags表结构

字 段 名说  明
tag_id标签ID
tag_name标签名称

表3-7 posts_tags表结构

字 段 名说  明
post_id对应的文章ID
tag_id对应的标签ID

为了找到同时属于“Java”、“MySQL”和“Redis”这3个标签的文章,需要使用如下的SQL语句:

SELECT p.post_titleFROM posts_tags pt,   posts p,   tags tWHERE pt.tag_id = t.tag_id AND (t.tag_name IN ('Java', 'MySQL', 'Redis')) AND p.post_id = pt.post_idGROUP 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所示{![集合类型键中元素是无序的,图3-18中为了便于读者阅读将元素按照大小顺序进行了排列。]}。


..18.tif{442}

图3-18 和标签有关部分的存储结构

最简单的,当需要获取标记“MySQL”标签的文章时只需要使用命令 SMEMBERS tag:MySQL:posts即可。如果要实现找到同时属于Java、MySQL和Redis 3个标签的文章,只需要将tag:Java:posts、tag:MySQL:posts和tag:Redis:posts这3个键取交集,借助SINTER命令即可轻松完成。

本文摘自《Redis入门指南》(第2版)

《Redis入门指南》(第2版)

本书是一本Redis的入门指导书籍,以通俗易懂的方式介绍了Redis基础与实践方面的知识,包括历史与特性、在开发和生产环境中部署运行Redis、数据类型与命令、使用Redis实现队列、事务、复制、管道、持久化、优化Redis存储空间等内容,并采用任务驱动的方式介绍了PHP、Ruby、Python和Node.js这4种语言的Redis客户端库的使用方法。

本书的目标读者不仅包括Redis的新手,还包括那些已经掌握Redis使用方法的人。对于新手而言,本书的内容由浅入深且紧贴实践,旨在让读者真正能够即学即用;对于已经了解Redis的读者,通过本书的大量实例以及细节介绍,也能发现很多新的技巧。