1

Redis数据类型介绍

Redis不是简单的键值存储,它实际上是一个数据结构服务器,支持各种类型的值。这意味着,尽管在传统的键值存储中,你将字符串键关联到字符串值,但是在Redis中,该值不仅限于简单的字符串,还可以保存更复杂的数据结构。以下是Redis支持的所有数据结构的列表,本教程将分别进行介绍:

  • 二进制安全字符串。
  • 列表:根据插入顺序排序的字符串元素集合,它们基本上是链表。
  • 集:唯一、未排序的字符串元素的集合。
  • 排序的集,类似于集,但每个字符串元素都与一个浮点数字值(称为分数)相关联。元素总是按它们的分数排序,因此与集不同,可以检索元素的范围(例如,你可能会问:给我前10个或后10个)。
  • 哈希,是由与值相关联的字段组成的映射,字段和值都是字符串,这与Ruby或Python哈希非常相似。
  • 位数组(或简称为位图):可以使用特殊命令像位数组一样处理字符串值:你可以设置和清除单个位、计数所有设置为1的位、找到第一个设置或未设置的位,等等。
  • HyperLogLogs:这是一种概率数据结构,用于估计集的基数,别害怕,它比看起来更简单...请参阅本教程后面的HyperLogLog部分。
  • 流:仅附加的类似映射的条目的集合,提供抽象的日志数据类型。

从命令参考中掌握这些数据类型的工作方式以及如何使用它们来解决给定问题并不总是那么容易,因此,本文档是Redis数据类型及其最常见模式的速成教程。对于所有示例,我们将使用redis-cli实用程序(一种简单但方便的命令行实用程序)针对Redis服务器发出命令。

Redis键

Redis键是二进制安全的,这意味着你可以使用任何二进制序列作为键,从“foo”之类的字符串到JPEG文件的内容,空字符串也是有效的键。

有关键的其他一些规则:

  • 太长的键不是一个好主意,例如,1024字节的键不仅在内存方面是一个坏主意,而且还因为在数据集中查找键可能需要进行几次代价高昂的键比较。即使手头的任务是匹配一个大值的存在,对它进行哈希(例如使用SHA1)也是一个更好的主意,尤其是从内存和带宽的角度来看。
  • 非常短的键通常不是一个好主意,如果你可以改写“user:1000:followers”,那么将“u1000flw”作为键写的就没有意义了。与键对象本身和值对象使用的空间相比,后者更具可读性,并且添加的空间较小,虽然短键显然会消耗较少的内存,但是你的工作是找到合适的平衡。
  • 试着坚持一个模式,例如,“object-type:id”是一个好主意,例如“user:1000”,点或破折号通常用于多字字段,例如“comment:10000:reply.to”或“comment:10000:reply-to”中。
  • 允许的最大键大小为512MB。

Redis字符串

Redis字符串类型是你可以与Redis键关联的最简单的值类型,它是Memcached中唯一的数据类型,因此对于新手来说,在Redis中使用它也是很自然的。

由于Redis键是字符串,因此当我们也使用字符串类型作为值时,我们会将一个字符串映射到另一个字符串,字符串数据类型对于许多用例很有用,例如缓存HTML片段或页面。

让我们使用redis-cli来研究字符串类型(在本教程中,所有示例都将通过redis-cli执行)。

> set mykey somevalue
OK
> get mykey
"somevalue"

如你所见,使用SETGET命令是我们设置和检索字符串值的方式,请注意,即使键已与非字符串值相关联,SET也会替换已存储在键中的任何现有值(即使键已存在),因此SET执行分配。

值可以是各种类型的字符串(包括二进制数据),例如,你可以在值内存储jpeg图像,值不能大于512MB。

SET命令具有有趣的选项,这些选项作为附加参数提供,例如,如果键已经存在,我可能会要求SET失败,或者相反,只有键已经存在时,它才会成功:

> set mykey newval nx
(nil)
> set mykey newval xx
OK

即使字符串是Redis的基本值,你也可以使用它们执行一些有趣的操作,例如,一个是原子增量:

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

INCR命令将字符串值解析为整数,将其递增1,最后将获得的值设置为新值,还有其他类似的命令,例如INCRBYDECRDECRBY,在内部,它始终是相同的命令,其执行方式略有不同。

INCR是原子的意味着什么?即使使用相同键发出INCR的多个客户端也永远不会进入竞争状态。例如,永远不会发生客户端1读取“10”,客户端2同时读取“10”的情况,两者都递增到11,并将新值设置为11。最终值将始终为12,并且在所有其他客户端未同时执行命令时执行读取递增设置操作。

有许多用于操作字符串的命令,例如,GETSET命令将键设置为新值,并返回旧值作为结果。你可以使用此命令,例如,如果你有一个系统,每次你的网站收到新访问者时都使用INCR来递增Redis键,你可能希望每小时收集一次此信息,而不丢失一个增量,你可以GETSET键,为其分配新值“0”,然后回读旧值。

在单个命令中设置或检索多个键的值的功能对于减少延迟也很有用,因此,有MSETMGET命令:

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

使用MGET时,Redis返回一个值数组。

修改和查询键空间

有些命令没有在特定类型上定义,但是在与键的空间进行交互时很有用,因此可以与任何类型的键一起使用。

例如,EXISTS命令返回1或0表示数据库中是否存在给定的键,而DEL命令则删除键和关联的值(无论该值是什么)。

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

从示例中,你还可以看到DEL本身如何返回1或0,具体取决于键已删除(存在)或未删除(不存在具有该名称的此类键)。

有许多与键空间相关的命令,但是以上两个是必不可少的,与TYPE命令一起使用,TYPE命令返回存储在指定键处的值的类型:

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

Redis expires:时间有限的键

在继续使用更复杂的数据结构之前,我们需要讨论另一个功能,该功能无论值类型如何都可以工作,并且称为Redis expires,基本上,你可以为键设置超时时间,这是有限的生存时间。生存时间过去后,该键将自动销毁,就像用户使用该键调用DEL命令一样。

有关Redis expires的一些快速信息:

  • 可以使用秒或毫秒精度进行设置。
  • 但是,到期时间分辨率始终为1毫秒。
  • 有关过期的信息被复制并保留在磁盘上,当你的Redis服务器保持停止状态时,时间实际上已经过去了(这意味着Redis保存了键过期的日期)。

设置过期时间很简单:

> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)

由于第二次调用延迟了5秒钟以上,因此两次调用之间的键消失了,在上面的示例中,我们使用EXPIRE来设置过期时间(也可以使用它来为已具有过期时间的键设置不同的过期时间,例如可以使用PERSIST来删除过期时间并使键永久保留)。但是,我们还可以使用其他Redis命令来创建带有过期时间的键,例如,使用SET选项:

> set key 100 ex 10
OK
> ttl key
(integer) 9

上面的示例设置了一个字符串值100的键,该键的过期时间为10秒,之后,调用TTL命令以检查键的剩余生存时间。

为了设置并检查以毫秒为单位的过期时间,请查看PEXPIREPTTL命令以及SET选项的完整列表。

Redis列表

为了解释List数据类型,最好从一些理论上入手,因为信息技术人员经常以不适当的方式使用List一词。例如,“Python列表”不是名称(链表)所建议的,而是数组(在Ruby中,相同的数据类型实际上称为数组)。

从非常普遍的角度来看,列表只是一个有序元素的序列:10,20,1,2,3是一个列表。但是,使用数组实现的列表的属性与使用链表实现的列表的属性非常不同,Redis列表通过链表实现。这意味着即使列表中有数百万个元素,在列表的开头或结尾添加新元素的操作也会在恒定时间内执行。使用LPUSH命令将新元素添加到具有10个元素的列表的开头的速度与将元素添加到具有1000万个元素的列表的开头的速度相同。

不利之处是什么?在使用数组实现的列表中,按索引访问元素的速度非常快(恒定时间索引访问),而在通过链表实现的列表中访问速度不是那么快(其中该操作需要与所访问元素的索引成比例的工作量)。

Redis列表是使用链表实现的,因为对于数据库系统而言,至关重要的是能够以非常快的方式将元素添加到很长的列表中,稍后你将看到,另一个强大的优势是Redis列表可以在恒定的时间内以恒定的长度获取。

当快速访问大量元素的中间很重要时,可以使用另一种数据结构,称为排序集,排序集将在本教程的后面部分介绍。

Redis列表的第一步

LPUSH命令将新元素添加到列表的左侧(顶部),而RPUSH命令将新元素添加到列表的右侧(尾部),最后,LRANGE命令从列表中提取元素范围:

> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"

请注意,LRANGE需要两个索引,要返回的范围的第一个和最后一个元素,两个索引都可以为负,告诉Redis从末尾开始计数:因此-1是列表的最后一个元素,-2是列表的倒数第二个元素,依此类推。

如你所见,RPUSH在列表的右侧附加了元素,而最后的LPUSH在左侧附加了元素。

这两个命令都是可变参数命令,这意味着你可以在单个调用中随意将多个元素推入列表中:

> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"

Redis列表上定义的一项重要操作是弹出元素的能力,弹出元素是同时从列表中检索元素并将其从列表中删除的操作,你可以从左侧和右侧弹出元素,类似于在列表的两侧推送元素的方式:

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

我们添加了三个元素,并弹出了三个元素,因此在此命令序列的最后,列表为空,没有其他要弹出的元素,如果我们尝试弹出另一个元素,这是我们得到的结果:

> rpop mylist
(nil)

Redis返回NULL值以表示列表中没有元素。

列表的常见用例

列表对于许多任务很有用,以下是两个非常有代表性的用例:

  • 记住用户发布到社交网络上的最新更新。
  • 使用消费者—生产者模式进行进程之间的通信,其中生产者将项目推入列表,而消费者(通常是工作者)消耗这些项目并执行动作,Redis具有特殊的列表命令,以使此用例更加可靠和高效。

例如,流行的Ruby库resquesidekiq都在后台使用Redis列表,以实现后台作业。

流行的Twitter社交网络将用户发布的最新推文添加到Redis列表中。

为了逐步描述一个常见的用例,假设你的主页显示了在照片共享社交网络中发布的最新照片,并且你想加快访问速度。

  • 每次用户发布新照片时,我们都会使用LPUSH将其ID添加到列表中。
  • 当用户访问主页时,我们使用LRANGE 0 9来获取最新发布的10个项目。

上限列表

在许多用例中,我们只想使用列表来存储最新项目,无论它们是什么:社交网络更新、日志或其他任何内容。

Redis允许我们使用列表作为上限集合,仅使用LTRIM命令记住最近的N个项目并丢弃所有最旧的项目。

LTRIM命令类似于LRANGE,但是不显示指定的元素范围,而是将范围设置为新列表值,给定范围之外的所有元素都将被删除。

一个例子将使它更加清楚:

> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

上面的LTRIM命令告诉Redis仅从索引0到2取列表元素,其他所有内容都将被丢弃,这允许一个非常简单但有用的模式:一起执行List push操作 + List trim操作,以便添加新元素并丢弃超出限制的元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上面的组合添加了一个新元素,并且仅将1000个最新元素添加到列表中,使用LRANGE,你可以访问最重要的项目,而无需记住非常旧的数据。

注意:虽然LRANGE从技术上来说是O(N)命令,但朝列表的开头或结尾访问较小范围是恒定时间操作。

列表上的阻塞操作

列表具有一项特殊功能,使其适合于实现队列,并且通常用作进程间通信系统的构建块:阻塞操作。

想象一下,你想通过一个进程将项目推入列表,然后使用一个不同的进程来对这些项目进行某种工作,这是通常的生产者 / 消费者设定,可以通过以下简单方式实现:

  • 为了将项目推送到列表中,生产者调用LPUSH
  • 为了从列表中提取 / 处理项目,消费者调用RPOP

但是,有时列表可能为空并且没有要处理的内容,因此RPOP仅返回NULL,在这种情况下,消费者被迫等待一段时间,然后使用RPOP重试,这被称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:

  • 强制Redis和客户端处理无用的命令(列表为空时的所有请求将无法完成任何实际工作,它们只会返回NULL)。
  • 由于工作者在收到NULL之后等待一段时间,因此增加了项目处理的延迟,为了减小延迟,我们可以在两次调用RPOP之间等待更少的时间,从而扩大了上述问题,即更多对Redis的无用调用。

因此,Redis实现了称为BRPOPBLPOP的命令,它们是RPOPLPOP的版本,如果列表为空,则可以阻塞这些命令:仅当将新元素添加到列表中或用户指定的超时时间到时,它们才会返回给调用者。

这是我们可以在工作者中使用的BRPOP调用的示例:

> brpop tasks 5
1) "tasks"
2) "do_something"

博弈
2.5k 声望1.5k 粉丝

态度决定一切