Redis官方文档(An Introduction to Redis data type and abstractions)翻译

原文链接

Redis 数据类型和抽象介绍

Redis不是一个普通的键值存储,它实际上是一个数据结构服务器,支持不同类型的值。这意味着,虽然在传统的键值存储中,您将字符串键与字符串值关联,但在Redis中,该值(value)不仅限于简单的字符串,还可以包含更复杂的数据结构。以下是Redis支持的所有数据结构的列表,本教程将分别介绍:

  • 二进制安全字符串(Binary-safe strings)
  • 列表(Lists):按插入顺序排序的字符串元素的集合。它本质上是链表。
  • 集合(Sets):唯一的、未排序的字符串元素的集合。
  • 排序集(Sorted sets),类似于集合,但是每个字符串元素都与一个浮点数值关联,称为score。元素总是按照它们的得分排序,因此与集合不同,可以检索一系列元素(例如,您可能会请求:给出前10个或后10个元素)。
  • 哈希表(Hashs),它是由字段及其关联的值组成的映射。字段和值都是字符串。这与Ruby或Python散列非常相似。
  • 位数组(或简单的位图)(Bit arrys):可以像位数组那样使用特殊的命令来处理字符串值,比如:可以设置和清除单个位,将所有位都计算为1,找到第一个设置或未设置位,等等。
  • HyperLogLogs:这是一种概率数据结构,用于估计集合的基数。不要害怕,这比看上去要简单得多。。。请参阅本教程后面的HyperLogLog部分。
  • 流(Streams):只追加类映射(map)条目(entry)的集合,这些条目提供了抽象的日志数据类型。在介绍Redis Streams(Redis 流)时,对它们进行了深入的介绍。

从命令参考手册(command reference)中了解这些数据类型的工作原理以及如何使用它们来解决给定的问题并不总是一件简单的事情,因此本文是Redis数据类型及其最常见模式的速成课程。

对于所有示例,我们将使用redis -cli实用程序(一个简单但方便的命令行实用工具)对Redis服务器发出命令。

Redis 键(Redis keys)

Redis键是二进制安全的,这意味着您可以使用任何二进制序列作为键,从字符串“foo”到JPEG文件的内容。空字符串也是一个有效的键。
关于键的一些其他规则:

  • 键特别长不是一个好主意。例如,一个1024字节的键不仅在内存方面是一个坏主意,而且因为在数据集中查找键可能需要几个代价高昂的键比较(key-comparisons)。特别是从内存和带宽的角度来考虑,即使手头的任务是匹配一个大值的存在,散列(例如用SHA1)也是一个更好的主意。
  • 很短的键通常不是一个好主意。如果可以编写“user:1000:follower”,那么编写“u1000flw”作为键就没有什么意义了。后者更具可读性,并且与键对象本身和值对象使用的空间相比,增添的空间较小。虽然短键显然会消耗更少的内存,但您的工作是键的可读性和存储空间上找到合适的平衡。
  • 试着坚持一个模式。例如,“object-type:id”是一个好主意,比如“user:1000”。点或破折号通常用于多字字段,如“comment?reply”,或者”comment?reply-to。

字符串(Redis Strings)

Redis字符串类型是可以与Redis键关联的最简单的值类型。它是Memcached中唯一的数据类型,所以对于新手来说,很自然地会在redis中想到它。

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

让我们使用redis-cli来演示一下字符串类型(本教程中的所有示例都将通过redis-cli来执行)。

> set mykey somevalue
OK
> get mykey
"somevalue"

如您所见,使用SET和GET命令是设置和检索字符串值的方式。注意,如果键已经存在,SET将替换存储在键中的任何现有值,即使键与非字符串值相关联。SET执行一个赋值。

值可以是各种类型的字符串(包括二进制数据),例如可以在值中存储jpeg图像。值不能大于512 MB。
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,最后将获得的值设置为新值。还有其他类似的命令,如INCRBY、DECR和DECRBY。在内部,它总是相同的命令,以稍微不同的方式执行。
  • INCR是原子意味着什么?即使多个客户端针对同一个密钥发出INCR,也永远不会进入竞争状态。例如,客户端1不会同时读取“10”,客户端2不会同时读取“10”,两者都增加到11,并将新值设置为11。最终值始终为12,并且执行了read- increment-set操作,在执行这些命令时不会同时执行其他客户机的命令。
  • 有许多用于操作字符串的命令。例如,GETSET命令为一键设置新值,最终返回旧值。例如,如果您的系统在每次web站点接收新访问者时都使用INCR递增Redis键,那么您可以使用这个命令。您可能希望每小时收集一次此信息,而不丢失任何增量。您可以GETSET键,将其赋值为“0”并读取旧值。
  • 在一个命令中设置或检索多个键值的能力对于减少延迟也很有用。因此,有MSET和MGET命令:
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

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

修改和查询键空间(Altering and querying the key space)

有些命令没有在特定类型上定义,但是对于与键空间交互很有用,因此可以与任何类型的键一起使用。
例如,EXISTS命令返回1或0来表示数据库中是否存在给定的键,而DEL命令则删除键和关联的值,无论该值是什么。

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

从示例中,您还可以看到DEL本身如何返回1或0,这取决于键是否被删除(存在则删除返回1)(没有具有该名称的键则无法删除返回0)。

与键空间相关的命令有很多,但是上面的两个是最基本的,还有TYPE命令,它返回存储在指定键上的值的类型:

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

(Redis 键超时)Redis expires: keys with limited time to live

在继续处理更复杂的数据结构之前,我们需要讨论另一个特性,它与值类型无关,称为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)

两个GET调用之间的键消失了,因为第二个调用延迟了5秒以上。在上面的示例中,我们使用EXPIRE来设置过期(也可以使用它来为已经设置过期的键设置其他操作,比如PERSIST可以用来删除过期并使键永久持久)。不过,我们也可以使用其他Redis命令创建带expires的密钥。例如使用SET选项:

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

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

为了设置和检查expires(以毫秒为单位),参考PEXPIRE和PTTL命令,以及set选项的完整列表。

列表(Redis Lists)

为了解释列表数据类型,最好从一些理论开始,因为信息技术人员经常以不恰当的方式使用列表这个术语。例如,“Python Lists”并不是名称所暗示的(链表),而是数组(实际上在Ruby中,相同的数据类型被称为数组)。

从一个非常普遍的观点来看,列表只是有序元素的序列:10、20、1、2、3是一个列表。但是使用数组实现的列表的属性与使用链表实现的列表的属性非常不同。

Redis列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的操作也是在常量时间内执行的。使用LPUSH命令向包含10个元素的列表的头部添加新元素的速度与向包含1000万个元素的列表的头部添加元素的速度相同。

缺点在哪?按索引访问元素在使用数组实现的列表中非常快(常量时间索引访问),而在使用链表实现的列表中则不那么快(在链表中,操作需要的工作量与所访问元素的索引成正例)。

Redis列表是用链表实现的,因为对于数据库系统来说,能够以非常快的速度向非常长的列表添加元素是非常重要的。另一个强大的优势是,如您稍后将看到的,Redis列表可以在恒定的时间内以恒定的长度使用。

当快速访问大型元素集合的中间部分很重要时,可以使用另一种不同的数据结构,称为排序集。本教程稍后将讨论排序集。

列表第一步(First steps with Redis Lists)

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返回一个空值,表示列表中没有元素。

列表的常用用例(Common use cases for lists)

列表对于一些任务是有用的,两个非常有代表性的用例是:

  • 记住用户在社交网络上发布的最新更新。
  • 进程之间的通信,使用消费者-生产者模式,生产者将项目推入列表,消费者(通常是工作人员)使用这些项目并执行操作。Redis有特殊的列表命令,使这个用例更加可靠和高效。

例如,流行的Ruby库resque和sidekiq都在底层使用Redis列表来实现后台作业。

流行的Twitter社交网络将用户发布的最新推文纳入Redis列表。

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

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

限制列表(Capped lists)

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

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

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之间的列表元素,其他的都将被丢弃。这允许一个非常简单但有用的模式:一起执行列表推送操作+列表修剪操作,以便添加一个新元素和丢弃超过限制的元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上面的组合添加了一个新元素,并且只接受列表中最新的1000个元素。使用LRANGE,您可以访问最上面的项目,而不需要记住非常旧的数据。

注意:虽然LRANGE在技术上是一个O(N)命令,但是访问列表头部或尾部的小范围是一个常量时间操作。

列表上的阻塞操作(Blocking operation on lists)

列表有一个特殊的特性,使它们适合实现队列,并且通常作为进程间通信系统的构建块(a building block):阻塞操作。
假设您想用一个进程将项目推入列表,并使用另一个进程来实际处理这些项目。这是通常的生产者/消费者设置,可以通过以下简单的方式实现:

  • 要将项目推入列表,生产者调用LPUSH。
  • 要从列表中提取/处理项目,使用者调用RPOP。

然而,有时列表可能是空的,没有什么要处理,所以RPOP只返回NULL。在这种情况下,消费者必须等待一段时间,然后再次尝试使用RPOP。这叫做轮询,在这种情况下不是个好主意,因为它有几个缺点:

  1. 强制Redis和客户机处理无用的命令(当列表为空时,所有的请求都不会完成实际的工作,它们只返回NULL)。
  2. 为项目的处理添加延迟,因为在工作人员接收到NULL之后,它会等待一段时间。为了使延迟更小,我们可以在调用RPOP之间等待更少的时间。从而放大问题1,即对Redis的更多无用调用。

因此,Redis实现了名为BRPOP和BLPOP的命令,这两个命令是RPOP和LPOP的阻塞版本,当列表为空时,它们能够阻塞:只有当向列表添加新元素或达到用户指定的超时时,它们才会返回给调用者。

这是一个我们可以在worker中使用的BRPOP调用的例子:

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

它的意思是:“等待列表任务中的元素,但如果5秒后没有可用的元素,则返回”。
注意,您可以使用0作为超时来永远等待元素,您还可以指定多个列表,而不仅仅是一个列表,以便同时等待多个列表,并在第一个列表接收到元素时得到通知。
关于BRPOP有几点需要注意:

  1. 客户端以有序的方式提供服务:阻塞等待列表的第一个客户端,在其他客户端推送某个元素时首先提供服务,等等。
  2. 与RPOP相比,返回值是不同的:它是一个双元素数组,因为它还包含键的名称,因为BRPOP和BLPOP能够阻塞等待从多个列表中的元素。
  3. 如果超时达到,则返回NULL。

关于列表和阻塞操作,还有更多的事情需要了解。我们建议你多阅读以下内容:

  • 可以使用RPOPLPUSH构建更安全的队列或旋转队列。
  • 该命令还有一个阻塞变体,称为BRPOPLPUSH。

自动创建和删除键(Automatic creation and removal of keys)

到目前为止,在我们的示例中,我们从来不必在推送元素之前创建空列表,或者在空列表中不再包含元素时删除它们。Redis的职责是在列表为空时删除键,或者在键不存在时创建一个空列表,并且我们试图向其中添加元素,例如,使用LPUSH。
这并不特定于列表,它适用于由多个元素组成的所有Redis数据类型——流、集、排序集和散列。
基本上我们可以用三条规则来总结这种行为:

  1. 当我们向聚合数据类型添加元素时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中删除元素后,如果值为空,则键将自动销毁。流数据类型是此规则的唯一例外。
  3. 调用诸如LLEN(它返回列表的长度)之类的只读命令,或者使用空键执行删除元素的写命令,总是会产生相同的结果,就好像该键持有的的空聚合类型是命令希望找到的类型一样。

规则1的例子:

> del mylist
(integer) 1
> lpush mylist 1 2 3
(integer) 3

但是,如果key存在,我们就不能对错误的类型执行操作:

> set foo bar
OK
> lpush foo 1 2 3
(error) WRONGTYPE Operation against a key holding the wrong kind of value
> type foo
string

规则2的例子:

> lpush mylist 1 2 3
(integer) 3
> exists mylist
(integer) 1
> lpop mylist
"3"
> lpop mylist
"2"
> lpop mylist
"1"
> exists mylist
(integer) 0

在弹出所有元素后,键不再存在。
规则3的例子:

> del mylist
(integer) 0
> llen mylist
(integer) 0
> lpop mylist
(nil)

散列(Redis Hashes)

Redis散列看起来就像人们期望的键值对“散列”一样:

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

虽然散列可以方便地表示对象,但实际上可以放入散列中的字段数量没有实际限制(除了可用内存),所以可以在应用程序中以多种不同的方式使用散列。

命令HMSET设置散列的多个字段,而HGET检索单个字段。HMGET类似于HGET,但返回一个值数组:

> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)

也有一些命令能够在单个字段上执行操作,比如HINCRBY:

> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997

您可以在文档中找到完整的散列命令列表。
值得注意的是,小哈希值(key对应的value比较小)即在内存中以特殊的方式进行编码,使它们的内存效率非常高。

集合(Redis Sets)

Redis集合是字符串的无序集合。SADD命令向集合添加新元素。还可以对集合执行许多其他操作,比如测试给定的元素是否已经存在,执行多个集合之间的交集、并集或差集等等。

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

这里,我向集合添加了三个元素,并告诉Redis返回所有元素。正如您所看到的,它们没有排序——Redis可以在每次调用时以任意顺序返回元素,因为它与用户没
有关于元素排序的约定。

Redis有测试成员状态的命令。例如,检查元素是否存在:

> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0

“3”是集合中的一员,而“30”不是。

集合很适合表示对象之间的关系。例如,我们可以很容易地使用集合来实现标记。

对这个问题建模的一个简单方法是为我们要标记的每个对象设置一个集合。该集合包含与对象关联的标记的id。

一个例子是给新闻文章加标签。如果文章ID 1000带有标签1、2、5和77,则一组可以将这些标签ID与新闻项关联起来:

> sadd news:1000:tags 1 2 5 77
(integer) 4

我们也可能想要有一个反向关系:所有的新闻列表用一个给定的标签标记:

> sadd tag:1:news 1000
(integer) 1
> sadd tag:2:news 1000
(integer) 1
> sadd tag:5:news 1000
(integer) 1
> sadd tag:77:news 1000
(integer) 1

获取给定对象的所有标记是很简单的:

> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2

注意:在本例中,我们假设您有另一个数据结构,例如Redis散列,它将标记id映射到标记名称。

使用正确的Redis命令还可以轻松实现其他一些重要的操作。例如,我们可能想要一个包含标签1、2、10和27的所有对象的列表。我们可以使用SINTER命令来实现这一点,它执行不同集合之间的交集。我们可以使用:

> sinter tag:1:news tag:2:news tag:10:news tag:27:news
... results here 

除了交集之外,还可以执行union、difference、提取随机元素等等。

提取元素的命令称为SPOP,可以方便地对某些问题进行建模。例如,为了实现一个基于web的扑克游戏,您可能需要用一个集合来表示您的牌组。假设我们对©lub、(D)iamond、(H)earts、(S)pades使用一个字符前缀:

>  sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK
   D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3
   H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6
   S7 S8 S9 S10 SJ SQ SK
   (integer) 52

现在我们要给每个玩家提供5张牌。SPOP命令删除一个随机元素,并将其返回给客户机,因此在本例中,这是一个完美的操作。

然而,如果我们直接对我们的牌组调用它,在下一个游戏中,我们将需要再次填充纸牌组,这可能不是理想的。首先,我们可以将存储在deck key中的集合复制到game:1:deck key中。

这是使用SUNIONSTORE来完成的,它通常在多个集合之间执行并集,并将结果存储到另一个集合中。但是,由于单个集合的并集本身就是一个集合,所以我可以用以下命令:

> sunionstore game:1:deck deck
(integer) 52

现在我准备给第一个玩家提供5张牌:

> spop game:1:deck
"C6"
> spop game:1:deck
"CQ"
> spop game:1:deck
"D1"
> spop game:1:deck
"CJ"
> spop game:1:deck
"SJ

一对jacks,不太好…

这是介绍set命令的好时机,set命令提供一个集合中元素的数量。在集合理论的上下文中,这通常称为集合的基数,因此Redis命令称为SCARD。

> scard game:1:deck
(integer) 47

计算结果是:52 - 5 = 47。

当您需要只获取随机元素而不从集合中删除它们时,有一个适合于该任务的SRANDMEMBER命令。它还具有同时返回重复和非重复元素的功能。

有序集(Redis Sorted sets)

有序集是一种数据类型,类似于集合和散列之间的混合。与集合类似,排序集合由惟一的、不重复的字符串元素组成,因此在某种意义上,有序集也是集合。
然而,虽然集合内的元素不是有序的,但已排序集合中的每个元素都与一个浮点值关联,称为score(这就是为什么类型也类似于散列,因为每个元素都映射到一个值)。
此外,有序集中的元素是按顺序获取的(所以它们不是按请求排序的,顺序是用来表示有序集的数据结构的一个特性)。它们的排列规则如下:

  • 如果A和B是两个得分不同的元素,如果A.score> B.score,那么A > B。
  • 如果A和B的分数完全相同,那么如果A字符串在词法上大于B字符串,则A > B。A和B字符串不能相等,因为排序集只有唯一的元素。

让我们从一个简单的例子开始,添加一些选定的黑客名称作为排序的集合元素,它们的出生年份作为“score”。

> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1

正如您所看到的,ZADD类似于SADD,但是接受一个额外的参数(放在要添加的元素之前),即分数。ZADD可以接收可变参数,所以您可以自由地指定多个score-value对,即使在上面的示例中没有使用它。

对于有序集,返回按出生年份排序的黑客列表是很简单的,因为实际上他们已经排序了。

实现提示:有序集是通过两个数据结构实现的,其中包含一个跳跃表和一个散列表,所以每次添加一个元素Redis都会执行O(log(N))操作。这很好,但当我们要求排序的元素Redis不需要做任何工作,它已经排序:

> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"

注意:0和-1表示从元素索引0到最后一个元素(-1在这里作用,就像在LRANGE命令中一样)。

如果我想要相反的顺序呢,从最小的到最大的?使用ZREVRANGE代替ZRANGE:

> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

也可以使用WITHSCORES参数返回分数:

> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"

范围操作

排序集在这方面的功能很强大。他们可以执行范围操作。让我们把1950年之前出生的所有人都包括在内。我们使用ZRANGEBYSCORE命令来完成:

> zrangebyscore hackers -inf 1950
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"

我们要求Redis返回得分在负无穷(negative infinity)到1950之间的所有元素(包括两个极端负无穷,正无穷)。

还可以删除元素的范围。让我们把1940年到1960年之间出生的所有黑客从排序集中删除:

> zremrangebyscore hackers 1940 1960
(integer) 4

ZREMRANGEBYSCORE可能不是最好的命令名,但它可能非常有用,并返回删除的元素数量。

另一个为有序集元素定义的非常有用的操作是get-rank操作。可以询问元素在有序元素集中的位置。

> zrank hackers "Anita Borg"
(integer) 4

考虑到按降序排列的元素,还可以使用ZREVRANK命令来获得降序的位置。

字典分数(Lexicographical scores)

在 Redis 2.8中,引入了一个新功能,假设元素相同的一组排序都插入相同的分数,则可以按字典顺序获取范围内的元素(元素比较大小使用C的memcmp函数,所以它保证没有排序,每个Redis实例将返回相同的输出)(elements are compared with the C memcmp function, so it is guaranteed that there is no collation, and every Redis instance will reply with the same output)。

使用词典范围操作的主要命令是ZRANGEBYLEX、ZREVRANGEBYLEX、ZREMRANGEBYLEX和ZLEXCOUNT。

例如,让我们再次添加我们的著名黑客列表,但这次对所有元素使用0分:

> zadd hackers 0 "Alan Kay" 0 "Sophie Wilson" 0 "Richard Stallman" 0
  "Anita Borg" 0 "Yukihiro Matsumoto" 0 "Hedy Lamarr" 0 "Claude Shannon"
  0 "Linus Torvalds" 0 "Alan Turing"

由于有序集排序规则,它们已经按字典顺序排序:

> zrange hackers 0 -1
1) "Alan Kay"
2) "Alan Turing"
3) "Anita Borg"
4) "Claude Shannon"
5) "Hedy Lamarr"
6) "Linus Torvalds"
7) "Richard Stallman"
8) "Sophie Wilson"
9) "Yukihiro Matsumoto"

使用ZRANGEBYLEX,我们可以请求字典范围某范围的元素:

> zrangebylex hackers [B [P
1) "Claude Shannon"
2) "Hedy Lamarr"
3) "Linus Torvalds"

范围可以包含或排除(取决于第一个字符),也可以用+和-字符串分别指定string infinite和- infinite。有关更多信息,请参阅文档。

这个特性很重要,因为它允许我们使用有序集作为泛型索引。例如,如果您想通过一个128位无符号整数参数为元素建立索引,那么您所需要做的就是将元素添加到一个具有相同得分(例如0)但带有16字节前缀的排序集中,该前缀由128位大端数字组成。由于数字以大端为单位,当按词法顺序(按原始字节顺序)排序时,实际上也是按数字顺序排序的,所以您可以查询128位空间中的范围,并获得去掉前缀的元素值。
如果您想在更严谨的上下文中查看该特性,请检查Redis autocomplete demo

更新分数:排行榜(Updating the score: leader boards)

在切换到下一个主题之前,请最后注意一下有序集。有序集的分数可以随时更新。只要对已包含在有序集中的元素调用ZADD,就会用O(log(N))时间复杂度更新其得分(和位置)。因此,当有大量更新时,有序集是合适的。

由于这个特性,一个常见的用例是排行榜。典型的应用程序是一个Facebook游戏,在这个游戏中,你结合了根据用户的高分对用户进行排序的功能,以及get-rank操作,以显示前n名用户,以及用户在排行榜中的排名(例如,“你是这里得分最高的4932名用户”)。

位图(Bitmaps)

位图不是实际的数据类型,而是在字符串类型上定义的一组面向位的操作。由于字符串是二进制安全的blob,其最大长度为512 MB,因此适合设置为 2 32 2^{32} 232个不同的位。

位操作分为两组:常量时间的单个位操作(比如将位设置为1或0,或者获取它的值)和位组的操作(比如在给定的位范围内计算集合的位的数量)。

位图最大的优点之一是,在存储信息时,位图通常可以节省大量空间。例如,在一个用增量用户id表示不同用户的系统中,仅使用512 MB内存就可以记住40亿用户的单个位信息(例如,知道用户是否想接收时事通讯)。

位的设置和检索使用SETBIT和GETBIT命令:

> setbit key 10 1
(integer) 1
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0

SETBIT命令的第一个参数是位编号,第二个参数是要将位设置为1或0的值。如果寻址位在当前字符串长度之外,该命令将自动扩大字符串。

GETBIT只返回指定索引处的位的值。超出范围的位(寻址超出存储在目标键中的字符串长度的位)总是被认为是零。

一组位上有三个操作命令:

  1. BITOP在不同字符串之间执行位操作。所提供的操作是AND、OR、XOR和NOT。
  2. BITCOUNT执行总体计数,报告设置为1的位的数量。
  3. BITPOS查找指定值为0或1的第一个位。

BITPOS和BITCOUNT都能够处理字符串的字节范围,而不是运行字符串的整个长度。下面是一个简单的BITCOUNT调用示例

> setbit key 0 1
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2

位图的常用用例有:

  • 各种实时分析。
  • 存储与对象id关联的空间高效,高性能的布尔信息。

例如,假设您想知道web站点用户每天访问的最长时间。你开始从0开始计算天数,这是你使你的网站公开的那一天,并且每次用户访问网站时用SETBIT设置一点。作为一个位索引,只需使用当前unix时间,减去初始偏移量,然后除以3600*24。

这样,对于每个用户,您都有一个包含每天访问信息的小字符串。使用BITCOUNT可以很容易地获得给定用户访问web站点的天数,而使用几个BITPOS调用,或者简单地获取和分析位图客户端,则可以很容易地计算最长的连续时间。

位图分解为多个键是很简单的,例如,为了对数据集进行分片,而且通常最好避免使用大型键。为了将位图分割到不同的键上,而不是将所有的位都设置为一个键,一个简单的策略就是每个键存储M个位,并使用位数/M获得键名,并使用位数MOD M在键内寻址第n位。

HyperLogLogs

HyperLogLog是一种概率数据结构,用于计算惟一的东西(技术上这指的是估计集合的基数)。通常,计算惟一项需要使用与要计算的项数成比例的内存,因为您需要记住在过去已经见过的元素,以避免重复计算它们。然而,有一组算法可以用内存交换精度:您以一个标准误差的估计度量结束,在Redis实现中,标准误差小于1%。这个算法的神奇之处在于,您不再需要使用与所计数项的数量成比例的内存,而是可以使用固定数量的内存!在最坏的情况下是12k字节,如果HyperLogLog(我们现在将其称为HLL)只看到很少的元素,则内存会少很多。

从概念上讲,HLL API类似于使用集合来完成相同的任务。您可以将每个得到的元素添加到一个集合中,并使用SCARD检查集合中元素的数量,这是惟一的,因为SADD不会重新添加现有的元素。

虽然您并没有真正地向HLL添加项,因为数据结构只包含一个状态(state)不包含实际元素,API是相同的:

  • 每次看到一个新元素时,都要使用PFADD将其添加到计数中。
  • 到目前为止,每当您希望检索PFADD中添加的惟一元素的当前近似值时,都要使用PFCOUNT。
> pfadd hll a b c d
(integer) 1
> pfcount hll
(integer) 4
  • 该数据结构的一个用例示例是统计用户每天在搜索表单中计算执行的惟一查询。
  • Redis也可以执行HLLs的联合,请查看完整的文档以获得更多信息。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值