Redis 数据类型
Redis 数据类型
Redis不仅仅是简单的键值对存储,它实际上是一个支持不同类型值的数据结构服务器。在传统键值存储中,我们只能将字符串键与字符串值相关联。而在Redis中,该值不仅限于简单的字符串,还可以容纳更复杂的数据结构。下面是Redis支持的所有的数据结构,本章节对常用的数据结构将会分别进行介绍
- 二进制安全字符串——Binary-Safe strings;
- 列表(lists): 根据插入顺序排序的字符串元素的集合。 基本上都是链表;
- 集合(sets):具有唯一性的未排序的元素的集合;
- 有序集合(sorted sets):和集合类似,不同的是有序集合中每一个元素都关联一个称为score的浮点型数值。集合里的元素按照这个score的值进行排序。所以和集合不同的地方是,有序集合可以按照指定的范围来检索元素。
- 哈希(hash):是将字段和值进行关联的一种映射,其中字段和值都是字符串;
- 位数组或简称位图(bitmaps):我们可以使用特殊的命令将字符串按照位来进行处理:我们可以设置或者清除某一位,对所有设置为1的位进行计数,找到第一个设置或者未设置的位等等;
- HyperLogLogs:这是一个具有概率性的数据结构,用于估计集合的基数;
- 流(streams): 只添加提供抽象日志数据类型的类似映射的元素的集合
掌握这些数据类型的工作方式,并且在给定特定问题之后,只是根据命令参考表就快速找出解决方法,这些是很困难的。所以本篇内容是一个关于Redis数据类型和它们最常见的模式的速成教程。
对于所有示例,我们将使用Redis官方自带的redis-cli(一种简单方便的命令行应用程序)来向Redis服务器发出操作命令。
Redis字符串(strings)
Redis字符串类型是我们可以与Redis键关联的最简单的值类型。在Memcached中,字符串是唯一的数据类型,因此对于初学者来说,在Redis中使用它也是很自然的。
由于Redis键是字符串,因此当我们也使用字符串类型作为值时,我们会将一个字符串映射到另一个字符串。字符串数据类型在许多应用场景中都很有用,例如缓存HTML片段或页面。
让我们使用redis-cli来蓼科一下字符串类型(在本教程中,所有示例都将通过redis-cli执行)。
redis 127.0.0.1:6379> set mykey jiyik
OK
redis 127.0.0.1:6379> get mykey
"jiyik"
如上所见,使用SET和GET命令是我们设置和检索字符串值的方式。请注意,即使键已经存在,并且是与非字符串类型的值进行的关联,SET仍会替换已存储在键中的任何现有值。
值可以是任意类型的字符串(包括二进制数据),例如,我们可以在值内存储jpeg图像。但是值的大小不能超过512 MB。
该SET命令还有一些附加参数。例如,如果键已经存在,我们可能会要求SET失败。或者是:只有键已经存在时,此次Set操作才会成功:
redis 127.0.0.1:6379> set mykey newval nx
(nil)
redis 127.0.0.1:6379> set mykey newval xx
OK
虽然Redis中的键名和键值都是基于字符串存储的,但是我们也可以使用一些命令来进行一些简便的操作。例如,实现一个是原子增量:
redis 127.0.0.1:6379> set counter 100
OK
redis 127.0.0.1:6379> incr counter
(integer) 101
redis 127.0.0.1:6379> incr counter
(integer) 102
redis 127.0.0.1:6379> incrby counter 50
(integer) 152
INCR命令将一个字符串值解析为一个整数,然后在此整数上面加1,并最终将加1后的值作为一个新的值,存到相对应的key里去还有其他类似的命令,例如INCRBY,DECR和DECRBY。它们是一类相同的命令,只是其执行方式略有不同。
INCR操作是原子性的,即使是多个客户端对相同的键名发出INCR的操作,这些客户端之间也不会出现竞争的状态。例如,永远不会发生客户端1读取“10”,客户端2也同时读取“ 10”,然后二者都递增到11,并将新值设置为11的情况。由于原子操作,所以最终值将始终为12,并且读取的值始终为最新的值,也就意味着这时是没有其他的客户端进行相同的操作。
有许多用于操作字符串的命令。例如,GETSET命令将键设置为新值,并将旧值作为返回结果。如果我们的系统在每次网站接收新访客时需要递增Redis key,则可以使用INCR命令。我们可能还希望每小时收集一次此信息,而又不会丢失任何增量。那就可以使用 GETSET key
,为其分配新值“0”,然后读取旧值。
在单个命令中设置或检索多个键的值的功能对于减少延迟是很有帮助的。因此,有MSET和MGET命令:
redis 127.0.0.1:6379> mset a 10 b 20 c 30
OK
redis 127.0.0.1:6379> mget a b c
1) "10"
2) "20"
3) "30"
使用MGET时,Redis返回的是一个包含所有值的数组。
Redis列表(lists)
为了解释List数据类型,我们最好从理论上入手,因为List经常被技术人员以不正确的方式使用。例如,“Python Lists”不是我们这里提到的列表的意思,而是数组(在Ruby中,相同的数据类型实际上也是称为数组)。
普遍来说,列表只是一个有序元素的序列:10,20,1,2,3是一个列表。但是,使用Array实现的List的属性与使用Linked List实现的List的属性是有很大不同的。
Redis列表是通过链接列表实现的。这意味着即使您在列表中有数百万个元素,在列表的开头或结尾添加新元素的操作也会在常量时间内执行。使用LPUSH命令将新元素添加到具有10个元素的列表的开头的速度与将元素添加到具有1000万个元素的列表的开头的速度相同。
不利之处是什么?在使用Array实现的列表中,按索引访问元素的速度非常快(常量时间索引访问),而在通过链接列表实现的列表中访问速度不是那么快(链接操作所需要的工作量与被访问元素的索引数量成比例)。
Redis列表是通过链接列表实现的,因为对于数据库系统而言,能够以非常快的方式将元素添加到很长的列表中是至关重要的。稍后我们将看到,另一个强大的优势是Redis列表可以在恒定的时间内以固定的长度获取。
当我们需要快速访问大量元素的中间的一些元素时,可以使用另一种称为有序集合的数据结构。有序集合将在本教程的后面部分介绍。
Redis列表的第一步
LPUSH命令将一个新元素添加到一个列表的左侧(也就是头部),而RPUSH命令将一个新元素添加到一个列表的右侧(也就是尾部)。最后,LRANGE命令从列表中提取一段范围内的元素:
redis 127.0.0.1:6379> rpush mylist A
(integer) 1
redis 127.0.0.1:6379> rpush mylist B
(integer) 2
redis 127.0.0.1:6379> lpush mylist first
(integer) 3
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
请注意,LRANGE需要两个索引,要返回的范围的第一个和最后一个元素。两个索引都可以为负,告诉Redis从末尾开始计数:因此-1是列表的最后一个元素,-2是列表的倒数第二个元素,依此类推。
如您所见,RPUSH在列表的右侧添加元素,LPUSH在列表的左侧添加加元素。
这两个命令都是可变参数命令,这意味着我们可以在单个调用中随意将多个元素推入列表中:
redis 127.0.0.1:6379> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
redis 127.0.0.1:6379> 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元素是同时从列表中检索元素并将其从列表中删除的操作。我们可以从左侧和右侧检索元素,类似于在列表两边添加元素的方式:
redis 127.0.0.1:6379> rpush mylist a b c
(integer) 3
redis 127.0.0.1:6379> rpop mylist
"c"
redis 127.0.0.1:6379> rpop mylist
"b"
redis 127.0.0.1:6379> rpop mylist
"a"
我们添加了三个元素并弹出了三个元素,因此在最后一个命令执行完成之后,列表为空,没有其他要弹出的元素。如果我们尝试弹出另一个元素,我们将会得到一个错误信息(Redis自定的空值):
redis 127.0.0.1:6379> rpop mylist
(nil)
Redis返回NULL值以表示列表中没有元素。
Redis哈希(Hashes)
Redis 的哈希看起来就喝哈希表一样,带有field-value对:
redis 127.0.0.1:6379> hmset user:1000 username antirez birthyear 1977 verified 1
OK
redis 127.0.0.1:6379> hget user:1000 username
"antirez"
redis 127.0.0.1:6379> hget user:1000 birthyear
"1977"
redis 127.0.0.1:6379> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
尽管哈希表很容易表示对象,但是实际上可以放入哈希表中的字段数没有实际限制(可用内存除外),因此我们可以在应用程序内部以多种不同方式使用哈希表。
HMSET命令设置哈希的多个字段,而HGET检索单个字段。HMGET类似于HGET,只是返回值是一个数组:
redis 127.0.0.1:6379> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)
有些命令也可以对单个字段执行操作,例如HINCRBY:
redis 127.0.0.1:6379> hincrby user:1000 birthyear 10
(integer) 1987
redis 127.0.0.1:6379> hincrby user:1000 birthyear 10
(integer) 1997
我们可以在文档中找到哈希命令的完整列表。
值得注意的是,小的哈希(即,一些具有较小值的元素)以特殊方式在内存中进行了编码,从而使其具有很高的存储效率。
Redis集合(sets)
Redis集合是字符串的无序集合。可以使用SADD命令向集合中添加新的元素。还可以对集合执行许多其他操作,例如测试给定元素是否已存在,执行多个集合之间的交集,并集或差等。
redis 127.0.0.1:6379> sadd myset 1 2 3
(integer) 3
redis 127.0.0.1:6379> smembers myset
1. 3
2. 1
3. 2
在这里,我们在集合中添加了三个元素,并告诉Redis返回所有元素。如您所见,它们没有排序-Redis可以在每次调用时随意以任何顺序返回元素,因为用户并没有通知Redis要对元素进行某种顺序的排序。
Redis具有用于测试集合中元素的命令。例如,检查元素是否存在:
redis 127.0.0.1:6379> sismember myset 3
(integer) 1
redis 127.0.0.1:6379> sismember myset 30
(integer) 0
“3”是集合的成员,而“30”不是集合的成员。
集合非常适合表示对象之间的关系。例如,我们可以轻松地使用集合来实现标签。
对这个问题进行建模的一种简单方法是为我们要标记的每个对象设置一个集合。该集合包含与对象关联的标签的ID。
一个例子是标记新闻文章。如果ID为1000的文章使用标签1、2、5和77进行标记,则可以使用一个集合将这些标签与文章的ID进行关联:
redis 127.0.0.1:6379> sadd news:1000:tags 1 2 5 77
(integer) 4
有些时候我们可能还需要逆关联:用给定标签标记的所有新闻的列表:
redis 127.0.0.1:6379> sadd tag:1:news 1000
(integer) 1
redis 127.0.0.1:6379> sadd tag:2:news 1000
(integer) 1
redis 127.0.0.1:6379> sadd tag:5:news 1000
(integer) 1
redis 127.0.0.1:6379> sadd tag:77:news 1000
(integer) 1
要获取给定对象的所有标签很简单:
redis 127.0.0.1:6379> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2
注意:在示例中,我们假设有另一个数据结构,例如Redis哈希,该结构将标签ID映射到标签名称。
使用正确的Redis命令,仍然很容易实现其他非常规的操作。例如,我们可能想要包含标签1、2、10和27在一起的所有对象的列表。我们可以使用SINTER命令执行此操作,该命令执行不同集合之间的交集。我们可以用:
redis 127.0.0.1:6379> sinter tag:1:news tag:2:news tag:10:news tag:27:news
... results here ...
除了交集,您还可以执行并集,求差,提取随机元素等等。
SPOP命令可以提取元素,这对于某些问题的建模是非常方便的。例如,为了实现基于Web的扑克游戏,我们可能需要用一个集合来代表牌组。想象一下,我们为(C)lubs, (D)iamonds, (H)earts, (S)pades使用一个单字符前缀:
redis 127.0.0.1:6379> 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命令删除一个随机元素,将其返回到客户端,所以在这种情况下完美运行。
但是,如果我们直接在终端上对其进行调用,那么在游戏的下一场比赛中,我们需要再次填充牌组,这可能不是我们理想的选择。因此,我们可以将存储在套牌键中的一组副本复制到game:1:deck
键的存储空间中。
这可以使用SUNIONSTORE命令完成。SUNIONSTORE通常执行多个集合之间的联合,并将结果存储到另一个集合中。但是,由于单个集合的并集还是其本身,所以我可以使用以下命令复制我的牌组:
redis 127.0.0.1:6379> sunionstore game:1:deck deck
(integer) 52
现在,我准备为第一位玩家提供五张牌:
redis 127.0.0.1:6379> spop game:1:deck
"C6"
redis 127.0.0.1:6379> spop game:1:deck
"CQ"
redis 127.0.0.1:6379> spop game:1:deck
"D1"
redis 127.0.0.1:6379> spop game:1:deck
"CJ"
redis 127.0.0.1:6379> spop game:1:deck
"SJ"
现在是引入一个新命令的好时机,该命令提供集合中元素的数量。这通常称为集合的基数,这个Redis命令为SCARD。
redis 127.0.0.1:6379> scard game:1:deck
(integer) 47
其中的数学原理:52-5 = 47
。
当您只需要获取随机元素而不将其从集合中删除时,可以使用适合该任务的SRANDMEMBER命令。它还具有返回重复和非重复元素的功能。
Redis有序集合(sorted sets)
有序集合也是一种数据类型,类似于集合和哈希之间的混合。像集合一样,有序集合由唯一的,非重复的字符串元素组成,因此从某种意义上说,有序集合也是一个集合。
但是,虽然不对集合内的元素进行排序,但是排序后的集合中的每个元素都与一个称为 score
的浮点值相关联(这就是为什么该类型也类似于哈希的原因,因为每个元素都映射到一个值)。
此外,有序集合中的元素是按顺序进行的(因此,它们不是应请求而排序的,顺序是用于表示已排序集合的数据结构的特殊性)。它们按照以下规则排序:
- 如果A和B是两个具有不同score值的元素,那么如果A.score > B.score,则A>B。
- 如果A和B的score完全相同,那么如果A字符串在字典上大于B字符串,则A>B。A和B字符串不能相等,因为有序集合中的元素具有唯一性。
让我们从一个简单的示例开始,添加一些选定的名称作为排序的集合元素,其出生年份为“score”。
redis 127.0.0.1:6379> zadd hackers 1940 "Alan Kay"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1953 "Richard Stallman"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1949 "Anita Borg"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1916 "Claude Shannon"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1969 "Linus Torvalds"
(integer) 1
redis 127.0.0.1:6379> zadd hackers 1912 "Alan Turing"
(integer) 1
如您所见,ZADD命令20270)与SADD命令相似,但是使用一个额外的参数(放置在要添加的元素之前)作为score的值。ZADD也是可变参数,因此即使在上面的示例中未使用它,您也可以自由指定多个 score-value 对。
使用有序集合,返回按其出生年份排序的名称列表很简单,因为实际上它们已经被排序了。
使用注意事项:有序集合是通过包含跳跃列表和哈希表的两种数据结构实现的,因此,每次添加元素时,Redis都会执行O(log(N))操作。
位图(bitmaps)
位图不是实际的数据类型,而是在String类型上定义的一组面向位的操作。由于字符串是二进制安全Blob,并且最大长度为512MB,因此它们适合设置多达2^32个不同的位。
位操作分为两类:固定时间的单个位操作(如将一个位设置为1或0或获取其值),以及对一组位的操作,例如,计算给定位范围内设置位的数量(例如,人口计数)。
位图的最大优点之一是,在存储信息时,它们通常可以节省大量空间。例如,在以增量用户ID表示不同用户的系统中,仅使用512MB内存就可以记住40亿用户的一位信息(例如,知道用户是否要接收新闻通讯)。
使用SETBIT32395)和GETBIT命令设置和检索位:
redis 127.0.0.1:6379> setbit key 10 1
(integer) 1
redis 127.0.0.1:6379> getbit key 10
(integer) 1
redis 127.0.0.1:6379> getbit key 11
(integer) 0
位图的常见用例是:
- 各种实时分析。
- 存储与对象ID相关联的可以节省空间并且高性能的布尔信息。
HyperLogLogs
HyperLogLog是一种概率数据结构,用于对唯一事物进行计数(从技术上讲,这是指估计集合的基数)。通常,对唯一项目进行计数需要使用与要计数的项目数量成比例的内存量,因为您需要记住过去已经检索的元素,以避免多次对其进行计数。但是,有一组算法会以内存换取精度:我们最终会得到带有标准误差的估计量,在Redis实现中,该误差小于1%。该算法的神奇之处在于,我们不再需要使用与所计数项目数量成正比的内存量,而是可以使用恒定数量的内存!在最坏的情况下为12k字节,如果我们的HyperLogLog(从现在开始将它们称为HLL
)看到的元素很少,则用到的内存将更少。