redis数据结构.docx
《redis数据结构.docx》由会员分享,可在线阅读,更多相关《redis数据结构.docx(30页珍藏版)》请在冰豆网上搜索。
![redis数据结构.docx](https://file1.bdocx.com/fileroot1/2022-11/25/3a0d2fd6-8dc9-4802-af10-23ba4f904a74/3a0d2fd6-8dc9-4802-af10-23ba4f904a741.gif)
redis数据结构
Redis数据结构和操作
redis不只是一个简单的键(key)-值(value)数据库,实际上它是一个数据结构服务器,支持各种类型的值。
也就是说,在传统的键-值数据库中,你把字符串键与字符串值联系起来,而在redis,值不仅限于一个简单的字符串,还可以是更复杂的数据结构。
下面列出了所有redis支持的数据结构,下文会分别对这些结构进行介绍:
∙二进制安全字符串
∙队列(lists):
基于插入顺序有序存储的字符串元素集合。
主要是链式的list。
∙集(sets):
元素唯一的、无序的字符串元素集合。
∙有序集(sortedsets):
与sets相似,但是每个字符串元素都与一个被称为分数(score)的浮点数相关联。
和sets不同的是,元素能够基于分数排序,因此可以检索某个范围内的元素(比如你可以查询前10个或后10个)。
∙哈希(hashes):
由域(fields)和值之间关系组成的映射。
域和值都是字符串。
这和Ruby或Python的哈希非常相似。
∙位数组(位图bitmaps):
可以通过特殊命令,像处理位图一样地处理字符串:
设置和清除某一位,统计被置1的位数,找到第一个被设置或没有被设置的位等。
∙HyperLogLogs:
这是一种概率数据结构,用于估算集的势。
不要被吓到了,没那么难。
本文将在下文中HyperLogLog章节介绍。
遇到问题的时候,理解数据结构是怎么工作的以及怎么被使用的并不是那么微不足道的事情。
因此,这篇文档是一个关于Redis数据类型和它们常用模式的速成教材。
这里所有的例子,我们都使用redis客户端(redis-cli)。
相对于redis服务器来说,这是一个简单方便的命令行控制台。
redis的键
redis的键是二进制安全【1】的,也说是说,你可以使用任意的二进制序列作为键,比如字符串”foo”或一个JPEG文件的内容。
空串也是一个有效的键。
一些关于键的其它规则:
∙太长的键不推荐。
例如长度为1024字节的键并不好,不管是从内存角度,还是从查询键的角度。
因为从数据集中查询键需要多次的键匹配步骤。
即使手边的任务就是要判断一个很大的值是否存在,采用某种手段对它做hash是个好主意,尤其是从内存和带宽的角度去考虑。
∙太短的键通常也不推荐。
如果你把键“user:
1000:
followers”写成“u1000flw”可能会有点问题。
因为前者可读性更好,而只需要多花费一点点的空间。
短的键显然占的花费的空间会小一点,因此你需要找到平衡点。
∙尽量坚持模式。
例如”object-type:
id”是推荐的,就像”user:
1000”。
点和短线常用于多个单词的场景,比如”comment:
1234:
reply.to”或”comment:
1234:
reply-to”。
∙键的大小不能超过512MB。
Redis中的字符串
Redis中的字符串类型是可以与键关联的最简单的类型。
它中Memcached中唯一的数据类型,也是Redis新手最常用的类型。
由于Redis的键都是字符串,那么把使用字符串为值,也就是字符串到字符串的映射。
字符串数据类型可以用于许多场景,比如缓存HTML片段或页面。
让我们用redis客户端尝试一些字符串类型的使用吧(本文所有的例子都在redis客户端执行)。
>setmykeysomevalue
OK
>getmykey
"somevalue"
∙1
∙2
∙3
∙4
正如你所看到的,GET和SET命令用于设置或获取一个字符串值。
需要注意的是,如果键已经存在,SET会覆盖它的值,即使与这个键相关联的不是字符串类型的值。
SET相当于赋值。
值可以是任意类型的字符串(包含二进制数据),你也可以使用一个jpeg图像。
值在大小不能大于512MB。
SET命令配上一些额外的参数,可以实现一些有趣的功能。
例如,我可以要求如果键已经存在,SET就会失败,或者相反,键已经存在时SET才会成功。
>setmykeynewvalnx
(nil)
>setmykeynewvalxx
OK
∙1
∙2
∙3
∙4
虽然字符串是最基础的数据类型,你仍可以对它执行一些有趣的操作,比如原子性的自增:
>setcounter100
OK
>incrcounter
(integer)101
>incrcounter
(integer)102
>incrbycounter50
(integer)152
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
INCR命令把字符串解析成一个整数,然后+1,把得到的结果作为一个新值存进去。
还有其它相似的命令:
INCRBY, DECR, DECRBY。
从命令的实现原理上讲,这几个命令是相同的,只是有一点细微的差别。
为什么说INCR是原子性的呢?
因为即使是多个客户端对同一个键使用INCR命令,也不会形成竞争条件。
举个例子,像这样的情况是不会发生的:
客户端1读取到键是10,客户端2也读到键值是10,它们同时对它执行自增命令,最终得到的值是11。
实际上,最终的得到的值是12,因为当一个客户端对键值做读-自增-写的过程中,其它的客户是不能同时执行这个过程的。
有许多用于操作字符串的命令,例如GETSET命令,它给键设置一个新值,并返回旧值。
比如你有一个系统,每当有一个新的访问者登陆你的网站时,使用INCR对一个键值自增。
你可能想要统计每个小时的信息,却又不希望丢失每次自增操作。
你可以使用GETSET命令,设置一个新值“0”,同时读取旧值。
redis支持通过一条命令同时设置或读取多个键,这对于减少延时很有用。
这就是MSET命令和MGET命令:
>mseta10b20c30
OK
>mgetabc
1)"10"
2)"20"
3)"30"
∙1
∙2
∙3
∙4
∙5
∙6
使用MGET时,redis返回包含多个值的数组。
更改或查询键空间
【2】
有些命令并没有指定特定的类型,但在与键空间的交互有非常有用,因此可以用于任意类型的键。
举个例子,EXISTS命令返回1或者0,用于表示某个给定的键在数据库中是否存在。
DEL命令删除键以及它对应的值而不管是什么值。
>setmykeyhello
OK
>existsmykey
(integer)1
>delmykey
(integer)1
>existsmykey
(integer)0
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
DEL返回1还是0取决于键是(键存在)否(键不存在)被删除掉了。
有许多键空间相关的命令,但以上这两个命令和TYPE命令是最基本的。
TYPE命令的作用是返回这个键的值的类型。
>setmykeyx
OK
>typemykey
string
>delmykey
(integer)1
>typemykey
none
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
键的生命周期
在介绍更多更复杂的数据结构之间,我们先讨论另一个与值类型无关的特性,那就是redis的期限(redisexpires)。
最基本的,你可以给键设置一个超时时间,就是这个键的生存周期。
当生存周期过去了,键会被自动销毁,就好像被用户执行过DEL一样。
一些关于redis期限的快速信息:
∙生存周期可以设置的时间单位从秒级到毫秒级。
∙生存周期的时间精度都是1毫秒。
∙关于生存周期的数据有多份且存在硬盘上,基于Redis服务器停止了,时间仍在流逝,这意味着redis存储的是key到期的时间。
设置生存周期是件琐碎的事情:
>setkeysome-value
OK
>expirekey5
(integer)1
>getkey(immediately)
"some-value"
>getkey(aftersometime)
(nil)
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
键在两次调用之间消失了,这是因为第二次调用的延迟了超过5秒的时间。
在上面的例子中,我们使用EXPIRE命令设置生命周期(它也可以用于为一个已经设置过生命周期的键重新设置生命周期,PERSIST命令可以用于移除键的命令周期,使它能够长期存在)。
我们还可以使用redis命令在创建键的同时设置生命周期。
比如使用带参数的SET命令:
>setkey100ex10
OK
>ttlkey
(integer)9
∙1
∙2
∙3
∙4
上面这个例子中创建了一个键,它的值是字符串100,生命周期是10秒。
后面的TTL命令用于查看键的剩余时间。
如果要以毫秒为单位设置或查询键的生命周期,请查询PEXPIRE命令和PTTL命令,以及SET命令的参数列表。
redis中的列表(lists)
要解释列表数据类型,最好先从一点理论开始。
因为列表这个术语常被信息技术人员错误地使用。
例如“python列表”,并不像它的命令所提示的(链表),而是数组(实际上与Ruby中的数组是同一个数据类型)。
从广义上讲,列表只是元素的有序序列:
10,20,1,2,3是一个列表。
但是用数组实现的列表和用链表实现的列表,它们的属性有很大的不同。
redis的列表都是用链表的方式实现的。
也就是说,即使列表中有数百万个元素,增加一个新元素到列表头部或尾部操作的执行时间是常数时间。
使用LPUSH命令把一个新元素增加到一个拥有10个元素的列表的头部,或是增加到一个拥有一千万个元素的列表的头部,其速度是一样的。
缺点是什么呢?
通过索引访问一个元素的操作,在数组实现的列表中非常快(常数时间),但在链表实现的列表中不是那么快(与找到元素对应下标的速度成比例)。
redis选择用链表实现列表,因为对于一个数据库来说,快速地向一个很大的列表新增元素是非常重要的。
另一个使用链表的强大优势,你稍后将会看到,能够在常数时间内得到一个固定长度的redis列表。
快速地读取很大一堆元素的中间元素也是重要的,这时可以使用另一种数据结构,称为有序集(sortedsets)。
本文后面会讲到有序集。
regis列表第一步
LPUSH命令把一个新的元素加到列表的左边(头部),而RPUSH命令把一个新的元素加到列表的右边(尾部)。
LRANGE命令从列表中提取某个范围内的元素
>rpushmylistA
(integer)1
>rpushmylistB
(integer)2
>lpushmylistfirst
(integer)3
>lrangemylist0-1
1)"first"
2)"A"
3)"B"
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
∙9
∙10
注意,LRANGE命令需要输入两个下标,即范围的第一个元素下标和最后一个元素下标。
两个下标都可以是负的,意思是从尾部开始数:
因此-1是最后一个元素,-2是倒数第二个元素,等。
正如你所见,RPUSH把元素加到列表右边,LPUSH把元素加到列表左边。
所有命令的参数都是可变的,即你可以随意地把多个增加元素入列表的命令放到一次调用中:
>rpushmylist12345"foobar"
(integer)9
>lrangemylist0-1
1)"first"
2)"A"
3)"B"
4)"1"
5)"2"
6)"3"
7)"4"
8)"5"
9)"foobar"
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
∙9
∙10
∙11
∙12
Redis中定义了一个重要的操作就是删除元素。
删除命令可以同时从列表中检索和删除元素。
你可以从左边或者右边删除元素,和从两边增加元素的方法类似:
>rpushmylistabc
(integer)3
>rpopmylist
"c"
>rpopmylist
"b"
>rpopmylist
"a"
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
我们增加和删除的三个元素,因此最后列表是空的,没有元素可以删除。
如果我们尝试继续删除元素,会得到这样的结果:
>rpopmylist
(nil)
∙1
∙2
redis返回空值说明列表中没有元素了。
列表的常见用例
列表可以用于完成多种任务,以下是两个非常有代表性的用例:
∙记住用户发布到社交网络的最新更新。
∙使用消费者-生产者模型进行进程间通信,生产生把表项(items)放进列表中,消费者(通常是工作者)消费这些items并执行一些行为。
redis针对这种用例有一些特殊的列表命令,既可靠又高效。
例如非常有名的Ruby库resque和sidekip,在底层都使用了Redis列表来实现后台作业。
著名的社交网络Twitter使用Redis列表来获取用户发布的最新的消息。
为了一步一步地描述一个常见用例,假设要在你的主页上展示社交网络上最新分享的照片并且加速访问。
∙每当一个用户发布了一张新的照片,我们使用LPUSH命令把它的ID加入到列表中。
∙当用户访问这个主页,我们使用LRANGE09获取最新加入的10个表项。
限制列表
很多情况下我们只想要使用列表来存储最新的几条表项,例如社交网络更新、日志或者其它。
Redis允许我们使用列表作为一个固定集合,使用LTRIM命令,只记录最新的N条记录,而丢弃所有更早的记录。
LTRIM命令和LRANGE命令相似,但不像LRANGE一样显示特定范围的元素,而是用这个范围内的值重新设置列表。
所有范围外的元素都被删除了。
用个例子来说明这一点:
>rpushmylist12345
(integer)5
>ltrimmylist02
OK
>lrangemylist0-1
1)"1"
2)"2"
3)"3"
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
上面的LTRIM命令告诉Redis只取得列表中下标为0到2的元素,其它的都要丢弃。
这就是一种简单有用的模式成为了可能:
列表增加(push)元素操作+列表提取(trim)元素操作=增加一个元素同时删除一个元素使得列表元素总数有限:
LPUSHmylist
LTRIMmylist0999
∙1
∙2
上面的操作结合增加一个元素但只是存在1000个最新的元素在列表中。
通过LRANGE你可以获取最新的表项而不需要记住旧的数据。
注意:
由于理论上LRANGE是O(N)命令,读取从头开始或从尾开始的小范围数据是常数时间的操作。
列表中是阻塞型操作
列表的一些特性使它适合现实队列(queues),也通常作为进程间通信系统的一个基础组件:
阻塞式操作。
假设你通过一个进程把元素增加到列表中,使用另一个进程对这些元素做些实际的操作。
这是通常的生产者/消费者基础,你可以用下面这种简单的方法实现:
∙生产者调用LPUSH,把元素加入列表
∙消费者调用RPOP,把元素从列表中取出或处理
有没有可能出现这种情况,列表是空的,没有什么东西可以处理,因此RPOP返回NULL。
这种情况下,消费者不得不等一会再尝试RPOP。
这就叫轮询。
这并不是一个好方法,因为它有以下缺点:
1.要求redis和客户端执行没有意义的命令(当列表为空是所有的请求都不会执行实际工作,只是返回NULL)
2.工作者在收到NULL之后加入一个延时,让它等待一些时间。
如果让延时小一点,在两次调用RPOP之间的等待时间会比较短,这成为第一个问题的放大-调用Redis更加没有意义
因此Redis实现了命令BRPOP和BLPOP,它是RPOP和LPOP的带阻塞功能的版本:
当列表为空时,它们会等到一个新的元素加入到列表时,或者用户定义的等待时间到了时,才会返回。
这是BRPOP调用的一个例子,我们可以在工作者进程使用它:
>brpoptasks5
1)"tasks"
2)"do_something"
∙1
∙2
∙3
它的意思是:
等待列表中的元素,如果5秒还没有可用的元素。
注意,如果使用0作为超时时间,将会永远等待,你也可以定义多个列表而不只是一个,这样就会同时等待多个列表,当任意一个列表收到一个元素时就会收到通知。
一些关于BRPOP需要注意的事情:
1.客户端是按顺序被服务的:
第一个等待某个列表的客户端,当列表被另一个客户端增加一个元素时,它会第一个处理。
2.返回值与RPOP的不同:
只得到两个元素的包含键名的数组,因为BRPOP和BLPOP因为等待多个列表而阻塞。
3.如果时间超时了,就会返回NULL
还有更多你应该知道的关于列表和阻塞操作的东西。
我们建议你阅读以下材料:
∙可以使用RPOPLPUSH创建更安全的队列或旋转队列。
∙这个命令有一个阻塞参数,即BRPOPLPUSH
自动创建和移除键
到目前为止我们的例子还没有涉及到这些情景,在增加一个元素之间创建一个空的列表,或者当一个列表没有元素时把它移除。
redis有责任删除变为空的列表,或当我们试图增加元素时创建空列表。
例如LPUSH
这不仅适用于列表,它可以应用于所有包含多个元素的Redis数据结构-集、有序集和哈希。
基本上讲,我们把它的行为总结为三个规则:
1.当我们把一个元素增加到一个集合类数据类型时,如果这个键不存在,在增加前会创建一个空的集合类数据类型。
2.我们从一个集合类数据类型中移除一个元素时,如果值保持为空,键就会被自动删除
3.调用一个只读命令例如LLEN(返回列表的长度),或者一个移除元素的写命令但键为空,结果不会改变。
[3]
规则1举例:
>delmylist
(integer)1
>lpushmylist123
(integer)3
∙1
∙2
∙3
∙4
然而,我们不能对一个已经存在的键执行与它类型不同的操作:
>setfoobar
OK
>lpushfoo123
(error)WRONGTYPEOperationagainstakeyholdingthewrongkindofvalue
>typefoo
string
∙1
∙2
∙3
∙4
∙5
∙6
规则2举例:
>lpushmylist123
(integer)3
>existsmylist
(integer)1
>lpopmylist
"3"
>lpopmylist
"2"
>lpopmylist
"1"
>existsmylist
(integer)0
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
∙9
∙10
∙11
∙12
当所有元素被取出,这个键就不存在了。
规则3举例:
>delmylist
(integer)0
>llenmylist
(integer)0
>lpopmylist
(nil)
∙1
∙2
∙3
∙4
∙5
∙6
redis中的哈希(hashed)
redis的哈希和我们所认识的“哈希”非常相似,是域-值对。
>hmsetuser:
1000usernameantirezbirthyear1977verified1
OK
>hgetuser:
1000username
"antirez"
>hgetuser:
1000birthyear
"1977"
>hgetalluser:
1000
1)"username"
2)"antirez"
3)"birthyear"
4)"1977"
5)"verified"
6)"1"
∙1
∙2
∙3
∙4
∙5
∙6
∙7
∙8
∙9
∙10
∙11
∙12
∙13
hash表示对象(object)非常方便。
实际上,可以放入一个hash的域的数量没有限制(不考虑可用内存),因此你可以在你的应用中用许多不同的方式使用哈希。
HMSET命令为hash设置多个域,而HGET获取某一个域。
HMGET和HGET相似,但它返回由值组成的数组。
>hmgetuser:
1000usernamebirthyearno-such-field
1)"antirez"
2)"1977"
3)(nil)
∙1
∙2
∙3
∙4
还有一些命令可以对单个的域执行操作,例如HINCRBY:
>hincrbyuser:
1000birthyear10
(integer)1987
>hincrbyuser:
1000birthyear10
(integer)1997
∙1
∙2
∙3
∙4
你可以查看这篇文档《hash命令全列》
把小的哈希(少量的元素,较小的值)用特殊的编码方式存放在内存中并不是什么难事,因此它们的空间效率非常高。
redis的集(sets)
Redis的集是字符串无序的集合。
SADD向集中增加一些元素。
对于集合还有很多其它的操作,例如测试某个给定的元素是否存在,多个集合之间求交集、合集或者差集,等。
>saddmyset123
(integer)3
>smembersmyset
1.3
2.1
3.2
∙1
∙2
∙3
∙4
∙5
∙6
在这个例子中,我向myset中增加了三个元素,并让redis返回所有的元素。
正如你所看到的,它们是无序的。
每次调用,redis都可能以任何顺序返回元素,因此在这里,用户不能对元素的顺序有要求。
redis提供测试成员的命令。
这个给定的元素是否存在?
>sismembermyset3
(integer)1
>sismembermyset30
(integer)0
∙1
∙2
∙3
∙4
“3”是这个集中的一员,而“30”不是。
集善于表现对象之间的关系。
例如我们可以很容易使用集实现标签(tags)。
处理这个问题的一个简单的模型就是把所有要打标签的对象设置一个集。
集包含相关对象的标签的ID。
假设我们想要为新闻加标签。
ID为1000的新闻被打上1,2,5和77这几个标签,我们可以用一个集将这些标签ID与新闻关联起来:
>saddnews:
1000:
tags12577
(integer)4
∙1
∙2
然而有时我会想要相反的关系:
列表中的所有新闻都被打上一个给定的标签:
>saddtag:
1:
news1000
(integer)1
>saddtag:
2:
news1000
(integer)1
>saddtag:
5:
news1000
(integ