阅读 58

Redis简介

Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息代理。它支持数据结构,例如字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,带有半径查询和流的地理空间索引。Redis具有内置的复制,Lua脚本,LRU驱逐,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供了高可用性。

您可以在这些类型上运行原子操作,例如追加到字符串。 在哈希中增加值; 将元素推送到列表; 计算集的交集,并集和差; 或在排序集中获得排名最高的成员。

为了获得出色的性能,Redis使用内存中的数据集。 根据您的用例,您可以通过将数据集偶尔转储到磁盘上,或者通过将每个命令附加到日志中来持久化它。如果只需要功能丰富的网络内存缓存,则可以选择禁用持久性。快速非阻塞的首次同步,在网上分裂时自动重连接与部分重同步。

其他功能包括:

  • 事务
  • 发布/订阅
  • LUA脚本
  • 带过期时间的key
  • 键的LRU缓存淘汰机制
  • 自动容灾机制

您可以从大多数编程语言中使用Redis。Redis是用ANSI C编写的,并且可以在大多数POSIX系统中使用,例如Linux,* BSD,OS X,而无需外部依赖。Linux和OS X是Redis开发和测试最多的两个操作系统,我们建议使用Linux进行部署。Redis可以在基于Solaris的系统中使用,例如SmartOS,但是尽力而为。 Windows版本没有官方支持。

1.Redis中的数据类型介绍

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

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

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

Redis Keys

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

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

Redis String

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

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

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

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

> 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,具体取决于key是否已删除(存在)(不存在具有该名称的此类key)。

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

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none
复制代码

Redis expires:key上加时效

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

  • 可以使用秒或毫秒精度进行设置。
  • 然而,过期时间解析总是1毫秒。
  • 有关过期的信息被复制并保留在磁盘上,实际上Redis服务器保持停止状态的时间已经过去(这意味着Redis保存了key过期的日期)。

设置过期时间很简单:

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

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

> set key 100 ex 10
OK
> ttl key
(integer) 9
复制代码

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

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

Redis Lists

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

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

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

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列表上定义的一项重要操作是pop元素的功能。 pop元素是同时从列表中检索元素并将其从列表中删除的操作。 您可以从左侧和右侧pop元素,类似于在列表两边push元素的方式:

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"
复制代码

我们添加了三个元素并弹出了三个元素,因此在此命令序列的末尾,列表为空,没有其他要弹出的元素。 如果我们尝试弹出另一个元素,则会得到以下结果:

> rpop mylist
(nil)
复制代码

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

列表的常用用例

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

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

例如,流行的Ruby库resque和sidekiq都在后台使用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之间等待更少的时间,从而扩大了问题编号1,即更多无用的Redis调用。

因此,Redis实现了称为BRPOP和BLPOP的命令,它们是RPOP和LPOP的版本,如果列表为空,它们将能够阻塞:仅当将新元素添加到列表中或用户指定的超时时间到时,它们才会返回到调用方。这是我们可以在worker中使用的BRPOP调用的示例:

> brpop tasks 5
1) "tasks"
2) "do_something"
复制代码

这意味着:“等待列表任务中的元素,但如果5秒钟后没有可用元素,则返回”。

请注意,您可以将0用作超时来永远等待元素,还可以指定多个列表,而不仅仅是一个列表,以便同时等待多个列表,并在第一个列表收到一个元素时得到通知。

有关BRPOP的几点注意事项:

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

关于列表和阻塞操作,您应该了解更多信息。 我们建议您阅读以下内容:

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

自动创建和删除键

到目前为止,在我们的示例中,我们无需在推送元素之前创建空列表,也无需在内部不再包含元素时删除空列表。Redis的责任是在列表为空时删除键,或者在键不存在的情况下创建一个空列表,并且我们正在尝试向其中添加元素,例如使用LPUSH。

这不是特定于列表的,它适用于由多个元素组成的所有Redis数据类型-流,集合,排序集合和哈希。 基本上,我们可以用三个规则来总结行为:

  • 当我们将元素添加到聚合数据类型时,如果目标键不存在,则在添加元素之前会创建一个空的聚合数据类型。
  • 当我们从聚合数据类型中删除元素时,如果该值保持为空,则键将自动销毁。 流数据类型是此规则的唯一例外。
  • 调用诸如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"0
复制代码

虽然散列可以方便地表示对象,但实际上可以放入散列中的字段数量没有实际限制(除了可用内存之外),因此可以在应用程序中以多种不同的方式使用散列。命令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
复制代码

值得注意的是,小散列(即一些具有较小值的元素)以特殊方式在内存中进行了编码,从而使它们具有很高的存储效率。

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 ...
复制代码

除了交集之外,您还可以执行并集,求差,提取随机元素等等。 提取元素的命令称为SPOP,它对于建模某些问题非常方便。 例如,为了实现基于Web的扑克游戏,您可能需要用一组来代表您的套牌。 假设我们对(C)lubs,(D)钻石,(H)耳钉,(S)垫使用一个单字符前缀:

>  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中的set复制到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"
复制代码

现在是引入set命令的好时机,该命令提供集合中元素的数量。 在集合论的上下文中,这通常称为集合的基数,因此Redis命令称为SCARD。

> scard game:1:deck
(integer) 47
复制代码

数学原理:52-5 = 47。

当您只需要获取随机元素而不将其从集合中删除时,可以使用适合该任务的SRANDMEMBER命令。 它还具有返回重复元素和非重复元素的功能。

Redis Sorted sets

排序集是一种数据类型,类似于集合和散列的混合。与集合一样,已排序的集合由惟一的、不重复的字符串元素组成,所以在某种意义上,已排序的集合也是一个集合。

但是,虽然集内的元素没有排序,但排序后的集合中的每个元素都与一个称为得分的浮点值相关联(这就是为什么该类型也类似于哈希的原因,因为每个元素都映射到一个值)。

此外,已排序集合中的元素是按顺序获取的(因此它们不是按请求排序的,顺序是用于表示已排序集合的数据结构的特性)。它们是根据下面的规则排列的

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

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

> 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也是可变参数,因此即使上面的示例中未使用它,您也可以自由指定多个得分-值对。

使用排序集,返回按其出生年份排序的黑客列表很简单,因为实际上他们已经被排序了。

实现注意:排序集是通过双端口数据结构实现的,其中包含一个跳跃列表和一个散列表,所以每次我们添加一个元素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"
复制代码

Operating on ranges

排序集比这更强大。 它们可以在范围内操作。 让我们获取所有在1950年(含)之前出生的人。我们使用ZRANGEBYSCORE命令来做到这一点:

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

我们要求Redis返回分数在负无穷大和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实例将以相同的输出进行回复)。使用词典范围操作的主要命令是ZRANGEBYLEX,ZREVRANGEBYLEX,ZREMRANGEBYLEX和ZLEXCOUNT。

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

> 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"
复制代码

范围可以是包含(inclusive)或排除(exclusive)(取决于第一个字符),字符串无限和负无限分别用+和-字符串指定。 有关更多信息,请参见文档。

此功能非常重要,因为它允许我们将排序后的集合用作通用索引。 例如,如果要通过128位无符号整数参数索引元素,则只需将元素添加到具有相同分数(例如0)但具有由128个字节组成的16字节前缀的排序集中 大尾数的位数。由于数字是以大尾数结尾的,当按词法顺序排列时(按原始字节顺序)实际上也是按数字顺序排列的,所以您可以请求128位空间中的范围,并获得元素的值,而不需要前缀。

更新分数:排行榜

在切换到下一个主题之前,请最后注意一下排序集。排序集的分数可以随时更新。只要对已经包含在排序集中的元素调用ZADD,就会用O(log(N))时间复杂度更新它的分数(和位置)。因此,当有大量更新时,排序集是合适的。由于这种特性,常见的用例是排行榜。 典型的应用是Facebook游戏,您可以将按高分对用户进行排序的能力与获得排名的操作结合起来,以显示前N名的用户以及排行榜中的用户排名(例如,“ 您是这里的#4932最佳成绩”)。

Bitmaps

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

位操作分为两类:固定时间的单个位操作(如将一个位设置为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只是返回指定索引处的位的值。 超出范围的位(寻址超出存储在目标键中的字符串长度的位)始终被视为零。

在位组上有三个命令:

  • BITOP在不同的字符串之间执行按位运算。 提供的运算为AND,OR,XOR和NOT。
  • BITCOUNT执行填充计数,报告设置为1的位数。
  • BITPOS查找指定值为0或1的第一位。

BITPOS和BITCOUNT都可以在字符串的字节范围内运行,而不是在字符串的整个长度上运行。 以下是BITCOUNT调用的一个简单示例:

> setbit key 0 1
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2
复制代码

位图的常见用例是:

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

例如,假设您想知道网站用户每天访问量最长的时间。 您从零开始计算天数,即从您公开网站的那一天开始,并在用户每次访问该网站时对SETBIT进行设置。 作为位索引,您只需花费当前的unix时间,减去初始偏移量,然后除以一天中的秒数(通常为3600 * 24)

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

位图很容易分成多个键,例如,为了分片数据集,并且因为通常最好避免使用大量键。 要在不同的键上拆分位图,而不是将所有位都设置为键,一个简单的策略是每个键存储M位,并获得具有位号/ M的键名,并使用bit来获取键内的第N位。 编号MOD M。

HyperLogLogs

HyperLogLog是一种概率数据结构,用于对唯一事物进行计数(从技术上讲,这是指估计集合的基数)。通常,对唯一项目进行计数需要使用与要计数的项目数量成比例的内存量,因为您需要记住过去已经看到的元素,以避免多次对其进行计数。但是,有一组算法会以内存为代价来交换精度:您最终会得到带有标准误差的估计量度,在Redis实现的情况下,该误差小于1%。该算法的神奇之处在于,您不再需要使用与计数的项目数量成比例的内存量,而是可以使用恒定数量的内存! 在最坏的情况下为12k字节,如果您的HyperLogLog(从现在开始将它们称为HLL)看到的元素很少,则少得多。

Redis中的HLL尽管在技术上是不同的数据结构,但被编码为Redis字符串,因此您可以调用GET来序列化HLL,然后调用SET来将其反序列化回服务器。

从概念上讲,HLL API就像使用Set来执行相同的任务。 您可以将每个观察到的元素添加到集合中,并使用SCARD检查集合中的元素数量,这是唯一的,因为SADD不会重新添加现有元素。

尽管您并未真正将项目添加到HLL中,但由于数据结构仅包含不包含实际元素的状态,因此API相同:

  • 每次看到新元素时,都可以使用PFADD将其添加到计数中。
  • 到目前为止,每次您要检索添加到PFADD中的唯一元素的当前近似值时,都使用PFCOUNT。
> pfadd hll a b c d
(integer) 1
> pfcount hll
(integer) 4
复制代码

该数据结构用例的一个例子是每天计算用户在搜索表单中执行的唯一查询。

Redis也能够执行HLL的合并,请查看完整的文档以获取更多信息。

Redis Stream

Stream是Redis 5.0引入的一种新数据类型,它以更抽象的方式对日志数据结构进行建模,但是日志的本质仍然完好无损:像日志文件一样,通常实现为仅在追加模式下打开的文件, Redis流主要是仅追加数据结构。至少从概念上讲,由于Redis是流式传输在内存中表示的抽象数据类型,因此它们实现了更强大的操作,以克服日志文件本身的限制。

尽管数据结构本身非常简单,但Redis流却成为最复杂的Redis类型的原因在于它实现了其他非强制性功能:一组阻塞操作,使消费者可以等待生产者将新数据添加到流中 ,此外,还有一个称为“消费群体”的概念。

消费者群体最初是由流行的称为Kafka(TM)的消息传递系统引入的。Redis用完全不同的术语重新实现了一个类似的想法,但是目标是相同的:允许一组客户合作使用同一消息流的不同部分。

基础

为了理解Redis流是什么以及如何使用它们,我们将忽略所有高级功能,而是根据用于操纵和访问它的命令来关注数据结构本身。基本上,这是大多数其他Redis数据类型(如列表,集合,排序集合等)所共有的部分。但是,请注意,列表也有一个可选的更复杂的阻塞API,由诸如BLPOP之类的命令导出。因此在这方面流与列表并没有太大的不同,只是额外的API更加复杂和强大。

由于流是仅追加数据结构,因此称为XADD的基本写入命令会将新条目追加到指定的流中。流条目不仅仅是一个字符串,而是由一个或多个字段-值对组成。这样,流的每个条目都已经结构化,就像以CSV格式编写的仅附加文件一样,其中每行中都有多个单独的字段。

> XADD mystream * sensor-id 1234 temperature 19.8
1518951480106-0
复制代码

上面对XADD命令的调用使用该命令返回的ID作为自动生成的条目ID向该键mystream的流中添加了一个条目``sensor-id: 1234, temperature: 19.8`,例子中返回的是1518951480106-0。作为第一个参数,键名称为mystream,第二个参数为条目ID,用于标识流中的每个条目。但是,在这种情况下,我们传递了*,因为我们希望服务器为我们生成一个新的ID。每个新ID都会单调增加,因此,更简单地说,与所有过去的条目相比,添加的每个新条目将具有更高的ID。服务器自动生成ID几乎总是您想要的,并且显式指定ID的情况非常少。稍后我们将详细讨论。事实上,每个Stream条目都有一个ID是与日志文件的另一个相似之处,可以使用行号或文件中的字节偏移量来标识给定的条目。回到XADD示例中,在键名和ID之后,接下来的参数是组成流条目的字段值对。

只需使用XLEN命令就可以获取Stream中的项目数:

> XLEN mystream
(integer) 1
复制代码

条目ID

XADD命令返回的条目ID,它唯一地标识给定流中的每个条目,它由两部分组成:

<millisecondsTime>-<sequenceNumber>
复制代码

毫秒时间部分实际上是生成流ID的本地Redis节点中的本地时间,但是,如果当前毫秒时间恰好小于前一个输入时间,则使用前一个输入时间,因此,如果时钟向后跳 单调递增的ID属性仍然有效。序列号用于在同一毫秒内创建的条目。 由于序列号是64位宽,因此实际上,在同一毫秒内可以生成的条目数没有限制。

这种ID的格式乍看起来可能很奇怪,而温柔的读者可能会怀疑为什么时间是ID的一部分。 原因是Redis流支持按ID进行范围查询。 由于ID与条目生成的时间有关,因此基本上可以免费查询时间范围。 我们将在介绍XRANGE命令时很快看到这一点。

如前所述,如果由于某种原因用户需要与时间无关但实际上与另一个外部系统ID相关联的增量ID,则XADD命令可以采用显式ID代替*触发自动生成的通配符ID, 如以下示例所示:

> XADD somestream 0-1 field value
0-1
> XADD somestream 0-2 foo bar
0-2
复制代码

请注意,在这种情况下,最小ID为0-1,并且该命令将不接受等于或小于前一个ID的ID:

> XADD somestream 0-1 foo bar
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item
复制代码

从流中获取数据

现在,我们终于可以通过XADD在流中添加条目。 但是,虽然将数据附加到流中是很明显的,但是查询流以提取数据的方式并不是很明显。如果继续使用日志文件的类比,一种明显的方法是模仿通常使用Unix命令tail -f进行的操作,也就是说,我们可能开始侦听以便获取附加到流中的新消息。 注意,不同于列表的阻塞操作,单个元素将放到一个进行pop类型的操作就像BLPOP命令的客户端,对于stream我们希望多个消费者可以看到追加到stream中的新消息,就像多个tail -f进程可以看到加入到日志中的内容。使用传统术语,我们希望流能够将消息扇出到多个客户端。

但是,这只是一种潜在的访问模式。 我们还可以以完全不同的方式看到流:不是作为消息传递系统,而是作为时间序列存储。在这种情况下,附加新消息也许也很有用,但是另一种自然查询模式是按时间范围获取消息,或者使用游标迭代消息以增量检查所有历史记录。这绝对是另一种有用的访问模式。

最后,如果我们从使用者的角度看待流,则可能希望以另一种方式访问该流,即可以将消息流划分为多个正在处理此类消息的使用者,以便 消费者组只能看到在单个流中到达的消息子集。这样,有可能在不同使用者之间扩展消息处理,而不必由单个使用者来处理所有消息:每个使用者将只需要处理不同的消息。 这基本上就是Kafka(TM)对消费者群体所做的事情。 通过用户组读取消息是从Redis流中读取消息的另一种有趣方式。

Redis流通过不同的命令支持上述所有三种查询模式。 下一节将显示所有这些内容,从最简单,更直接的使用开始:范围查询。

按范围查询:XRANGE和XREVRANGE

要按范围查询流,只需要指定两个ID(开始和结束)即可。返回的范围将包含以ID开头或结尾的元素,因此该范围包括在内。两个特殊ID-和+分别表示可能的最小和最大ID。

> XRANGE mystream - +
1) 1) 1518951480106-0
   2) 1) "sensor-id"
      2) "1234"
      3) "temperature"
      4) "19.8"
2) 1) 1518951482479-0
   2) 1) "sensor-id"
      2) "9999"
      3) "temperature"
      4) "18.2"
复制代码

返回的每个条目都是两个项目的数组:ID和字段值对列表。

我们已经说过条目ID与时间有关,因为-字符左侧的部分是创建流条目的本地节点在创建条目之时的Unix时间(以毫秒为单位)(但是请注意 使用完全指定的XADD命令复制流,因此从属设备将具有与主设备相同的ID)。这意味着我可以查询一段时间范围使用XRANGE. 为了做到这一点,我可能省略ID的序列部分,如果省略,范围的开始部分会假设为0,范围的结束部分会假设为最大值。这样,只需使用2毫秒的Unix时间进行查询,我们以包含性的方式获得了在该时间范围内生成的所有条目。例如,我可能想查询我可以使用的两个毫秒的周期:

> XRANGE mystream 1518951480106 1518951480107
1) 1) 1518951480106-0
   2) 1) "sensor-id"
      2) "1234"
      3) "temperature"
      4) "19.8"
复制代码

我在此范围内只有一个条目,但是在实际数据集中,我可以查询小时的范围,或者在短短两毫秒内可以有很多项目,并且返回的结果可能很大。 因此,XRANGE最后支持一个可选的COUNT选项。通过指定一个计数,我只能得到前N个项目。 如果需要更多,可以获取返回的最后一个ID,将序列部分加1,然后再次查询。让我们在下面的示例中看到它。 我们开始使用XADD添加10个项目(我不会显示,已经假设流mystream中填充了10个项目)。为了开始我的迭代,每个命令获得2个项目,我从整个范围开始,但count为2。

> XRANGE mystream - + COUNT 2
1) 1) 1519073278252-0
   2) 1) "foo"
      2) "value_1"
2) 1) 1519073279157-0
   2) 1) "foo"
      2) "value_2"
复制代码

为了继续进行下两个项目的迭代,我必须选择返回的最后一个ID,即1519073279157-0,并在ID的序列号部分加1。 请注意,序列号是64位,因此无需检查溢出。在这种情况下,生成的ID(在这种情况下为1519073279157-1)现在可以用作下一个XRANGE调用的新开始参数:

> XRANGE mystream 1519073279157-1 + COUNT 2
1) 1) 1519073280281-0
   2) 1) "foo"
      2) "value_3"
2) 1) 1519073281432-0
   2) 1) "foo"
      2) "value_4"
复制代码

依此类推。 由于XRANGE复杂度是O(log(N))来寻找,然后是O(M)来返回M个元素,因此,该命令的计数很少,因此具有对数时间复杂度,这意味着迭代的每个步骤都很快。 因此,XRANGE也是事实上的流迭代器,不需要XSCAN命令。

命令XREVRANGE与XRANGE等效,但是以相反的顺序返回元素,因此XREVRANGE的实际用法是检查Stream中的最后一项:

> XREVRANGE mystream + - COUNT 1
1) 1) 1519073287312-0
   2) 1) "foo"
      2) "value_10"
复制代码

请注意,XREVRANGE命令以相反的顺序使用start和stop参数。

使用XREAD收听新项目

当我们不想按流中的某个范围访问项目时,通常我们想要的是订阅到达该流的新项目。这个概念可能与您订阅频道的Redis发布/订阅有关,或者与Redis阻止列表有关,在其中您等待键以获取新元素,但是使用流的方式存在根本差异:

  • 一个流可以有多个客户端(消费者)在等待数据。 默认情况下,每个新项目都将交付给每个正在等待给定流中数据的使用者。 此行为与阻止列表不同,在阻止列表中,每个使用者都将获得不同的元素。 但是,向多个消费者传播的能力类似于Pub / Sub。
  • 当在发布/订阅消息中时,消息会被触发并忘记,并且永远不会被存储;而在使用阻止列表时,当客户端接收到一条消息时,该消息会从列表中弹出(有效删除),流的工作方式从根本上来说是不同的。所有消息将无限期地附加到流中(除非用户明确要求删除条目):不同的使用者将通过记住收到的最后一条消息的ID,从其角度了解什么是新消息。
  • 流使用者组提供了一个发布/订阅列表或阻止列表无法实现的控制级别,对于同一流,不同的组具有对处理流的显式确认,检查待处理项的能力,对未处理消息的声明以及每个视图的连贯历史可见性 单一客户端,只能查看其过去的私人邮件历史记录。

提供侦听进入流的新消息的功能的命令称为XREAD。它比XRANGE复杂一点,因此我们将开始显示简单的表单,稍后将提供整个命令布局。

> XREAD COUNT 2 STREAMS mystream 0
1) 1) "mystream"
   2) 1) 1) 1519073278252-0
         2) 1) "foo"
            2) "value_1"
      2) 1) 1519073279157-0
         2) 1) "foo"
            2) "value_2"
复制代码

上面是XREAD的非阻塞形式。请注意,COUNT选项不是强制性的,实际上命令的唯一强制性选项是STREAMS选项,它指定键的列表以及调用方使用者已经为每个流看到的相应最大ID。 仅向客户提供ID大于我们指定的ID的消息。

在上面的命令中,我们写入了STREAMS mystream 0,因此我们希望流mystream中的所有消息的ID都大于0-0。 如上例所示,该命令返回键名,因为实际上可以用一个以上的键调用此命令以同时从不同的流中读取.我可以写例如:STREAMS mystream otherstream 0 0。请注意,在STREAMS选项之后,我们如何需要提供keys名称以及以后的ID。因此,STREAMS选项必须始终为最后一个。

除了XREAD可以一次访问多个流,而且我们能够指定我们拥有的最后一个ID来获取更新的消息之外,这种简单形式的命令与XRANGE相比并没有什么不同。但是,有趣的是,通过指定BLOCK参数,我们可以轻松地在阻塞命令中启用XREAD:

> XREAD BLOCK 0 STREAMS mystream $
复制代码

请注意,在上面的示例中,除了删除COUNT之外,我还指定了新的BLOCK选项,其超时时间为0毫秒(表示永不超时)。此外,我没有传递流mystream的常规ID,而是传递了特殊ID $。这个特殊的ID意味着XREAD应该使用流mystream中已经存储的最大ID作为最后一个ID,以便从我们开始侦听开始,我们将仅接收新消息。在某种程度上,这类似于tail -f Unix命令。

请注意,使用BLOCK选项时,我们不必使用特殊ID 。我们可以使用任何有效的ID。如果该命令能够立即满足我们的请求而不会阻塞,那么它将执行此命令,否则它将阻塞。通常,如果要使用从新条目开始的流,则从ID开始,然后我们继续使用收到的最后一条消息的ID进行下一个调用,依此类推。

XREAD的阻塞形式还能够通过指定多个键名来侦听多个Stream。如果由于至少有一个流的元素大于我们指定的相应ID而可以同步处理该请求,则它将返回结果。否则,该命令将阻塞并返回获取新数据的第一个流的项目(根据指定的ID)。

与阻塞列表操作类似,从语义上来说,先进先出的原则是,从等待数据的客户端的角度来看,阻塞流读取是公平的。第一个为给定流阻塞的客户端是第一个将在新项目可用时被阻塞的客户端。

XREAD除了COUNT和BLOCK之外没有其他选项,因此这是一个非常基本的命令,其特定目的是将使用者附加到一个或多个流。使用消费者组API可以获得消费流的更强大的功能,但是通过消费者组的读取是由另一个名为XREADGROUP的命令实现的,该命令将在本指南的下一节中介绍。

消费者组

当手头的任务是消耗来自不同客户端的相同流时,XREAD已经提供了一种扇出到N个客户端的方法,可能还使用从设备来提供更多的读取可伸缩性。但是,在某些问题中,我们要做的不是向多个客户端提供相同的消息流,而是向同一客户端提供从同一流中不同的消息子集。一个明显有用的情况是处理消息速度较慢的情况:拥有N个不同的工作程序将接收流的不同部分的能力使我们能够通过将不同的消息路由到准备执行的不同工作程序来扩展消息处理 更多的工作。实际上,如果我们假设有三个使用者C1,C2,C3和包含消息1、2、3、4、5、6、7的流,那么我们想要的是像下图中那样提供消息 :

1 -> C1
2 -> C2
3 -> C3
4 -> C1
5 -> C2
6 -> C3
7 -> C1
复制代码

为了达到这种效果,Redis使用了一个叫做消费者组的概念。 非常重要的一点是,从与Kafka(TM)消费群的实施角度来看,Redis消费群与之无关,但从实现的概念来看,它们只是相似的,因此我决定 与最初流行这种想法的软件产品相比,不要改变术语。一个消费者组就像一个伪消费者,它从一个流中获取数据,并实际服务于多个消费者,提供一定的保证:

  • 每个消息都提供给不同的使用者,因此不可能将同一消息传递给多个使用者。
  • 在消费者组中,消费者是通过名称来标识的,名称是实现消费者的客户必须选择的区分大小写的字符串。这意味着,即使在断开连接后,流使用者组也将保留所有状态,因为客户端将再次声称自己是同一使用者。但是,这也意味着由客户端提供唯一的标识符。
  • 每个消费者组都有从未消费的第一个ID的概念,因此,当消费者请求新消息时,它只能提供以前从未交付过的消息。
  • 然而,消费消息需要使用特定的命令显式确认,例如:此消息已正确处理,因此可以从消费者组中清除。
  • 消费者组跟踪所有当前挂起的消息,即已传递到消费者组的某个消费者但尚未被确认为已处理的消息。借助此功能,在访问流的消息历史记录时,每个消费者只会看到传递给它的消息。

从某种意义上讲,可以将消费者组想象为流的某种状态:

+----------------------------------------+
| consumer_group_name: mygroup           |
| consumer_group_stream: somekey         |
| last_delivered_id: 1292309234234-92    |
|                                        |
| consumers:                             |
|    "consumer-1" with pending messages  |
|       1292309234234-4                  |
|       1292309234232-8                  |
|    "consumer-42" with pending messages |
|       ... (and so forth)               |
+----------------------------------------+
复制代码

如果您从这种角度看待这一点,那么很容易理解消费者群体可以做什么,如何能够仅向消费者提供其待处理消息的历史记录,以及如何向消费者要求新消息的消息ID大于last_delivered_id。同时,如果将消费者组视为Redis流的辅助数据结构,则很明显,单个流可以具有多个消费者组,这些消费者组具有一组不同的消费者。实际上,对于同一流,甚至有可能使客户端通过XREAD读取而不使用消费者组,而客户端通过XREADGROUP在不同的消费者组中进行读取。现在是时候放大查看基本的消费者组命令了,这些命令如下:

  • XGROUP用于创建,销毁和管理消费者组。
  • XREADGROUP用于通过消费者组从流中读取。
  • XACK是允许消费者将待处理消息标记为正确处理的命令。

创建消费者组

假设我已经有一个键为mystream的类型的流,为了创建消费者组,我需要执行以下操作:

> XGROUP CREATE mystream mygroup $
OK
复制代码

正如您在上面的命令中看到的那样,在创建消费者组时,我们必须指定一个ID,在示例中,该ID仅为。之所以需要这样做,是因为在其他状态中,消费者组必须了解在第一个消费者连接处接下来要处理的消息,即刚创建该组时当前的最后一条消息ID是什么?如果我们像以前那样提供,则仅从现在开始流中到达的新消息将提供给组中的消费者。如果我们指定0,那么使用者组将使用流历史中的所有消息作为开始。当然,您可以指定任何其他有效的ID。 您所知道的是,消费者组将开始传递大于您指定的ID的消息.因为表示流中当前最大的ID,所以指定将只使用新消息。

XGROUP CREATE还支持使用可选的MKSTREAM子命令作为最后一个参数来自动创建流(如果不存在)。

> XGROUP CREATE newstream mygroup $ MKSTREAM
OK
复制代码

现在已经创建了消费者组,我们可以立即开始使用XREADGROUP命令尝试通过消费者组读取消息。我们将从消费者那里得知,我们将其称为Alice和Bob,以了解系统如何将不同的消息返回给Alice和Bob。

XREADGROUP与XREAD非常相似,并提供相同的BLOCK选项,否则为同步命令。但是,必须始终指定一个强制性选项,它是GROUP,并且具有两个参数:消费者组的名称和尝试读取的消费者的名称。还支持选项COUNT,它与XREAD中的选项相同。 在从流中读取信息之前,让我们在其中添加一些消息:

> XADD mystream * message apple
1526569495631-0
> XADD mystream * message orange
1526569498055-0
> XADD mystream * message strawberry
1526569506935-0
> XADD mystream * message apricot
1526569535168-0
> XADD mystream * message banana
1526569544280-0
复制代码

注意:此处的message是字段名称,水果是关联的值,请记住,流项目是小词典。 现在该尝试使用消费者组来阅读一些东西了:

> XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream >
1) 1) "mystream"
   2) 1) 1) 1526569495631-0
         2) 1) "message"
            2) "apple"
复制代码

XREADGROUP答复就像XREAD答复一样。 但是请注意,上面提供的GROUP <group-name> <consumer-name>,它指出我想使用消费者组mygroup从流中读取,我是消费者Alice。消费者每次对消费者组执行操作时,都必须指定其名称,以在组内唯一标识该消费者。

上面的命令行中还有一个非常重要的细节,在强制性STREAMS选项之后,为key mystream请求的ID是特殊ID>。这个特殊的ID只在消费者组的上下文中有效,它的意思是:到目前为止还没有向其他消费者传递过消息。

这几乎总是您想要的,但是也可以指定一个真实的ID,例如0或任何其他有效的ID,但是在这种情况下,发生的情况是我们要求XREADGROUP仅向我们提供未决消息的历史记录 ,在这种情况下,该组将永远不会看到新消息.因此,基本上,XREADGROUP基于我们指定的ID具有以下行为:

  • 如果该ID是特殊ID>,那么该命令将仅返回到目前为止从未发送给其他消费者的新消息,并且,副作用是将更新消费者组的最后一个ID。
  • 如果该ID是任何其他有效的数字ID,则该命令将使我们能够访问待处理消息的历史记录。 也就是说,已传递给此指定消费者的消息集(由提供的名称标识),到目前为止尚未用XACK确认。

我们可以立即将ID指定为0来测试此行为,而无需使用任何COUNT选项:我们只会看到唯一的待处理消息,即关于苹果的消息:

> XREADGROUP GROUP mygroup Alice STREAMS mystream 0
1) 1) "mystream"
   2) 1) 1) 1526569495631-0
         2) 1) "message"
            2) "apple"
复制代码

但是,如果我们确认该消息已被处理,它将不再成为挂起的消息历史记录的一部分,因此系统将不再报告任何内容:

> XACK mystream mygroup 1526569495631-0
(integer) 1
> XREADGROUP GROUP mygroup Alice STREAMS mystream 0
1) 1) "mystream"
   2) (empty list or set)
复制代码

如果您还不知道XACK的工作原理,请不要担心,其概念仅仅是处理过的消息不再是我们可以访问的历史的一部分。 现在轮到鲍勃读一些东西了:

> XREADGROUP GROUP mygroup Bob COUNT 2 STREAMS mystream >
1) 1) "mystream"
   2) 1) 1) 1526569498055-0
         2) 1) "message"
            2) "orange"
      2) 1) 1526569506935-0
         2) 1) "message"
            2) "strawberry"
复制代码

鲍勃最多要求两个消息,并且正在通过同一组mygroup进行阅读。因此,发生的事情是Redis仅报告新消息。如您所见,“苹果”消息未传递,因为它已经传递给了爱丽丝,所以鲍勃得到了橙子和草莓,依此类推。

这样,爱丽丝(Alice),鲍勃(Bob)和组中的任何其他使用者都可以从同一流中读取不同的消息,以读取其尚未处理的消息的历史记录,或将消息标记为已处理。 这允许创建不同的拓扑和语义以使用流中的消息。有几件事要牢记:

  • 消费者是在第一次被提及时自动创建的,无需显式创建。
  • 即使使用XREADGROUP,您也可以同时读取多个键,但是要使此键起作用,您需要在每个流中创建一个具有相同名称的使用者组。
  • XREADGROUP是一种写命令,因为即使它从流中读取,使用者组也会被修改为读取的副作用,因此只能在主实例中调用它。

从永久性故障中恢复

上面的示例使我们能够编写参与同一消费者组的消费者,将每个消息子集都进行处理,并从故障中恢复,重新读取刚发送给他们的未决消息。消费者的挂起消息在任何原因停止后都无法恢复,会发生什么情况?但是,在现实世界中,消费者可能会永久失败,永远无法恢复。消费者的挂起消息在任何原因停止后都无法恢复,会发生什么情况?

Redis消费者组提供了一个用于这些情况的特性,用于声明给定消费者的未决消息,以便此类消息将更改所有权并重新分配给不同的消费者。该功能非常明确,使用者必须检查待处理消息列表,并且必须使用特殊命令声明特定的消息,否则服务器将永远将已分配的待处理消息分配给旧使用者,这样不同的应用程序可以 选择是否使用此类功能,以及使用方式的确切选择。

此过程的第一步只是一个命令,该命令可提供使用者组中待处理条目的可观察性,称为XPENDING。这只是一个只读命令,始终可以安全调用,并且不会更改任何消息的所有权。以最简单的形式,仅使用两个参数调用该命令,这两个参数是流的名称和使用者组的名称:

> XPENDING mystream mygroup
1) (integer) 2
2) 1526569498055-0
3) 1526569506935-0
4) 1) 1) "Bob"
      2) "2"
复制代码

以这种方式调用时,该命令仅输出使用者组中待处理消息的总数,在这种情况下,仅输出两个消息,待处理消息中的较低和较高的消息ID,最后是使用者列表和待处理消息的数量 他们有。我们只有Bob带有两条未决消息,因为Alice请求的唯一消息是使用XACK确认的。

我们可以通过给XPENDING提供更多参数来请求更多信息,因为完整的命令签名如下:

XPENDING <key> <groupname> [<start-id> <end-id> <count> [<consumer-name>]]
复制代码

通过提供一个开始和结束ID(在XRANGE中可以是-和+)和一个计数来控制命令返回的信息量,我们可以了解有关挂起消息的更多信息。如果希望将输出限制为仅针对给定使用者的待处理消息,则使用可选的最终参数使用者名称,但在以下示例中将不使用此功能。

> XPENDING mystream mygroup - + 10
1) 1) 1526569498055-0
   2) "Bob"
   3) (integer) 74170458
   4) (integer) 1
2) 1) 1526569506935-0
   2) "Bob"
   3) (integer) 74170458
   4) (integer) 1
复制代码

现在我们有每条消息的详细信息:ID,使用者名称,空闲时间(以毫秒为单位),这是自从上次将消息传递给某个使用者以来已经过去了多少毫秒,最后是给定消息的次数 邮件已传递。我们收到了来自Bob的两条消息,它们闲置了74170458毫秒,大约20个小时。

请注意,没有人阻止我们仅使用XRANGE来检查第一条消息的内容。

> XRANGE mystream 1526569498055-0 1526569498055-0
1) 1) 1526569498055-0
   2) 1) "message"
      2) "orange"
复制代码

我们只需要在参数中重复两次相同的ID。 现在我们有了一些想法,爱丽丝可能会决定,在20个小时不处理消息之后,鲍勃可能无法及时恢复,现在是时候声明此类消息并代替鲍勃恢复处理了。 为此,我们使用XCLAIM命令。

该命令非常复杂,并且具有完整形式的选项,因为它用于复制使用者组更改,但是我们仅使用通常需要的参数。 在这种情况下,就像这样调用一样简单:

XCLAIM <key> <group> <consumer> <min-idle-time> <ID-1> <ID-2> ... <ID-N>
复制代码

基本上,我们说,对于这个特定的密钥和组,我希望指定的消息ID将更改所有权,并将其分配给指定的使用者名称<consumer>。但是,我们还提供了最短的空闲时间,因此只有在所提及消息的空闲时间大于指定的空闲时间时,该操作才能起作用。这很有用,因为可能有两个客户端正在尝试同时声明一条消息:

Client 1: XCLAIM mystream mygroup Alice 3600000 1526569498055-0
Client 2: XCLAIM mystream mygroup Lora 3600000 1526569498055-0
复制代码

但是,声明一条消息会产生副作用,这将重置其空闲时间! 并且将增加其交付数量计数器,因此第二个客户将无法要求它。这样,我们避免了消息的琐碎重新处理(即使在一般情况下您无法获得完全一次的处理)。

这是命令执行的结果:

> XCLAIM mystream mygroup Alice 3600000 1526569498055-0
1) 1) 1526569498055-0
   2) 1) "message"
      2) "orange"
复制代码

该消息已由Alice成功声明,该消息现在可以处理并确认该消息,并且即使原始使用方无法恢复,也可以将其向前移动。 从上面的示例可以明显看出,作为成功声明给定消息的副作用,XCLAIM命令也会返回该消息。 但是,这不是强制性的。可以使用JUSTID选项以便仅返回成功声明的消息的ID。如果您想减少客户端和服务器之间使用的带宽,这很有用,还要执行命令,而且您对消息不感兴趣,因为您的使用者的实现方式是不时重新扫描挂起消息的历史记录。

声明也可以通过一个单独的过程来实现:该过程仅检查待处理消息的列表,并将空闲消息分配给看似活动的使用者。

流的可观察性

缺乏可观察性的消息系统很难使用。 不知道谁在消费消息,哪些消息在等待处理,给定流中活动的消费者组集合使一切变得不透明。因此,Redis流和消费者群体使用不同的方式来观察正在发生的事情。我们已经介绍了XPENDING,它使我们可以检查在给定时刻正在处理的消息列表,以及它们的空闲时间和传递次数。

但是,我们可能还想做更多的事情,并且XINFO命令是一个可观察性接口,可以将其与子命令一起使用,以获取有关流或使用者组的信息。

此命令使用子命令以显示有关流及其使用者组状态的不同信息。例如

> XINFO STREAM mystream
 1) length
 2) (integer) 13
 3) radix-tree-keys
 4) (integer) 1
 5) radix-tree-nodes
 6) (integer) 2
 7) groups
 8) (integer) 2
 9) first-entry
10) 1) 1526569495631-0
    2) 1) "message"
       2) "apple"
11) last-entry
12) 1) 1526569544280-0
    2) 1) "message"
       2) "banana"
复制代码

输出显示有关流内部编码方式的信息,还显示流中的第一条消息和最后一条消息。可用的另一信息是与此流值关联的消费者组的数量。我们可以进一步询问有关消费者群体的更多信息。

> XINFO GROUPS mystream
1) 1) name
   2) "mygroup"
   3) consumers
   4) (integer) 2
   5) pending
   6) (integer) 2
2) 1) name
   2) "some-other-group"
   3) consumers
   4) (integer) 1
   5) pending
   6) (integer) 0
复制代码

如您在此以及上一个输出中所看到的,XINFO命令输出一系列字段值项。因为这是一个可观察性命令,所以它使人类用户可以立即了解所报告的信息,并允许该命令将来通过添加更多字段来报告更多信息,而不会破坏与旧客户端的兼容性。相反,其他必须提高带宽效率的命令(例如XPENDING)仅报告信息而没有字段名称。

上面示例的输出(使用GROUPS子命令)应清楚观察字段名称。我们可以通过检查特定消费者组中已注册的消费者来更详细地检查其状态。

> XINFO CONSUMERS mystream mygroup
1) 1) name
   2) "Alice"
   3) pending
   4) (integer) 1
   5) idle
   6) (integer) 9104628
2) 1) name
   2) "Bob"
   3) pending
   4) (integer) 1
   5) idle
   6) (integer) 83841983
复制代码

如果您不记得命令的语法:

> XINFO HELP
1) XINFO <subcommand> arg arg ... arg. Subcommands are:
2) CONSUMERS <key> <groupname>  -- Show consumer groups of group <groupname>.
3) GROUPS <key>                 -- Show the stream consumer groups.
4) STREAM <key>                 -- Show information about the stream.
5) HELP                         -- Print this help.
复制代码

与Kafka (TM)分区的区别

Redis流中的消费者组可能在某种程度上类似于基于Kafka(TM)分区的消费者组,但是请注意,Redis流实际上是非常不同的。分区仅是逻辑分区,消息仅放在单个Redis密钥中,因此为不同客户端提供服务的方式取决于谁准备处理新消息,而不是从客户端从哪个分区读取。例如,如果使用者C3在某个时刻永久失败,则Redis将继续为C1和C2提供所有到达的新消息,就像现在只有两个逻辑分区一样。

同样,如果给定的使用者处理消息的速度比其他使用者快得多,则该使用者将在相同的时间单位内按比例接收更多消息。这是可能的,因为Redis明确跟踪所有未确认的消息,并记住谁收到了哪个消息以及第一个消息的ID从未传递给任何使用者。

但是,这也意味着在Redis中,如果您确实要将同一个流中的消息划分为多个Redis实例,则必须使用多个密钥和某些分片系统,例如Redis Cluster或某些其他特定于应用程序的分片系统。单个Redis流不会自动分区到多个实例。我们可以说,以下事实是正确的:

  • 如果您使用1个流-> 1个使用者,则按顺序处理消息。
  • 如果您对N个使用者使用N个流,以便只有给定的使用者命中N个流的子集,则可以缩放上述模型,即1个流-> 1个使用者。
  • 如果使用1个流-> N个使用者,则负载均衡到N个使用者,但是在那种情况下,有关同一逻辑项的消息可能会被无序使用,因为给定的使用者处理消息3的速度比另一个使用者处理的速度更快 讯息4。

因此,基本上,Kafka分区与使用N个不同的Redis key更相似。 虽然Redis使用者组是一个服务器端负载平衡系统,用于将消息从给定的流发送到N个不同的使用者。

上限流

许多应用程序不想永远将数据收集到流中。有时,在流中最多具有给定数量的项很有用,而有时达到给定大小后,将数据从Redis移到内存中的存储就不是很有用,存储速度不快但适合于 可能数十年的历史。Redis流对此提供了一些支持。一种是XADD命令的MAXLEN选项。 此选项使用非常简单:

> XADD mystream MAXLEN 2 * value 1
1526654998691-0
> XADD mystream MAXLEN 2 * value 2
1526654999635-0
> XADD mystream MAXLEN 2 * value 3
1526655000369-0
> XLEN mystream
(integer) 2
> XRANGE mystream - +
1) 1) 1526654999635-0
   2) 1) "value"
      2) "2"
2) 1) 1526655000369-0
   2) 1) "value"
      2) "3"
复制代码

使用MAXLEN,当达到指定的长度时,旧的条目将被自动清除,这样流的大小将保持不变。目前没有告诉流只保留不超过给定数量的项的选项,因为这样的命令为了一致地运行,可能需要阻塞很长时间才能驱逐项。想象一下,例如,如果有一个插入峰值,然后是一个长时间的暂停,然后是另一个插入,它们都有相同的最长时间,会发生什么。该流将阻塞来驱逐在暂停期间变得太旧的数据。因此,这取决于用户做一些规划,并了解什么是所需的最大流长度。此外,虽然流的长度与所使用的内存成正比,但按时间进行修整并不太容易控制和预期:它取决于插入率,而插入率是随时间变化的变量(当其不变时, 只需按大小修剪即可)。

但是,用MAXLEN进行修整可能会很昂贵:为了使内存效率很高,流由宏节点表示为基数树。更改由几十个元素组成的单个宏节点并不是最佳选择。 因此可以以以下特殊形式给出命令:

XADD mystream MAXLEN ~ 1000 * ... entry fields here ...
复制代码

在MAXLEN选项和实际计数之间的〜参数意味着,我实际上并不需要精确地将其设为1000个项目。它可以是1000或1010或1030,只需确保至少保存1000个项目即可。使用此参数,仅当我们可以删除整个节点时才执行修剪。这使其效率更高,通常就是您想要的。

还有一个可用的XTRIM命令,其执行的功能与上面的MAXLEN选项非常相似,但是该命令不需要添加任何内容,它可以以独立方式针对任何流运行。

> XTRIM mystream MAXLEN 10
复制代码

或者,对于XADD选项:

> XTRIM mystream MAXLEN ~ 10
复制代码

但是,即使当前仅实现了MAXLEN,XTRIM仍被设计为接受不同的修整策略。假定这是一条明确的命令,将来可能会允许按时间指定修整,因为以独立方式调用此命令的用户应该知道自己在做什么。

XTRIM应该具有的一种有用的驱逐策略可能是通过一系列ID删除的能力。目前这是不可能的,但将来可能会实现,以便在需要时更轻松地一起使用XRANGE和XTRIM将数据从Redis移至其他存储系统。

从流中删除单个项目

流还具有一个特殊的命令,仅通过ID即可从流的中间删除项。 通常,对于仅附加的数据结构,这可能看起来很奇怪,但是对于涉及例如隐私法规的应用程序,它实际上很有用。 该命令称为XDEL,它将仅获取流的名称,后跟要删除的ID:

> XRANGE mystream - + COUNT 2
1) 1) 1526654999635-0
   2) 1) "value"
      2) "2"
2) 1) 1526655000369-0
   2) 1) "value"
      2) "3"
> XDEL mystream 1526654999635-0
(integer) 1
> XRANGE mystream - + COUNT 2
1) 1) 1526655000369-0
   2) 1) "value"
      2) "3"
复制代码

但是,在当前的实现中,直到宏节点完全为空时才真正回收内存,因此您不应滥用此功能。

零长度流

流与其他Redis数据结构之间的区别在于,当其他数据结构不再具有元素时,作为调用除去元素的命令的副作用,key本身将被除去。取而代之的是,由于使用MAXLEN选项(计数为零)(XADD和XTRIM命令)或由于调用了XDEL,因此流被保留为零元素。当前,即使没有关联的使用者组,流也不会被删除,但是将来可能会改变。

其他值得注意的特性

Redis API中还有其他重要内容,在本文档的上下文中无法探讨,但值得您注意:

  • 可以逐步迭代大型集合的键空间。详见
  • 可以在服务器端运行Lua脚本以改善延迟和带宽。详见
  • Redis还是Pub-Sub服务器。详见

感谢您的阅读,并祝您使用Redis玩得开心!