0x000 概述
Redis
不是一个简单键值对存储器,而是一个数据结构服务,它支持不同类型的值。这意味着传统的键值对存储器将字符串键和字符串值关联起来,在Redis
中,值的类型不仅仅局限于字符串,还可以是更加复杂的数据结构,下面是Redis
支持的数据结构,将会在各个章节接触到:
- 字节安全的字符串
- 列表:根据插入顺序排序的字符串集合元素,是最基本的链表
- 集合:唯一的无序的字符串元素
- 有序集合:和集合很想但是每个字符串元素都关联着一个浮点型数字作为值,叫做分数。他总是按照分数排序,所以它不想集合那样,获取一个范围之内的元素。
- 哈希:是一个由键值对关联起来的
map
,键和值都是字符串。对于Ruby
和Python
非常的友好。 - 比特数组:使用特殊的命令可以向处理比特数组一样处理字符串,你可以设置或者清除独立的比特,将所有的比特设置为1,找到第一个比特等等等
- HyperLogLogs:不解释。
知道这些数据类型和怎样使用对于解决命令索引给出的问题并不总是微不足道的。知道这些数据类型和怎样使用对于解决命令索引给出的问题是很重要的,所以这个文档将作为了解Redis
数据类型和他们基本模式的一个入门课程。
对于所有的案例我们将使用redis-cli
工具,这是一个很简单但是很便利的命令行工具,用来和Redis
服务端做交互。
0x001 Redis
的key
Redis
的key
是比特安全的,这意味着你可以使用任何的二进制序列作为key
,从像foo
的字符串到一个JPEG
文件的内容。甚至空字符串也是可以的。
关于key
有一些其他的规则:
- 非常长的
key
是不推荐的。一个1024 bytes
是一个非常坏的注意,不仅仅是因为内存浪费,更是因为在数据集中搜索对比的时候需要耗费更多的成本。当要处理的是匹配一个非常大的值,从内存和带宽的角度来看,使用这个值的hash
值是更好的办法(比如使用SHA1
)。 - 特别短的
key
通常也是不推荐的。在写像u100flw
这样的键的时候,有一个小小的要点,我们可以用user:1000:followers
代替。可读性更好,对于key
对象和value
对象增加的空间占用与此相比来说倒是次要的。当短的key
可以很明显减少空间占用的时候,你的工作就是找到正确的平衡 - 尝试去固定一个密室。比如
object-type:id
是一个好主意,-
和.
通常用于多个字符的域,就像comment:1234:reply.to
,或者comment:1234:reply-to
。 - 最大的
key
允许512MB
0x002 Redis
字符串
Redi
字符串类型是Redis
的key
可以关联的的最简单的数据类型。这是Mmcached
唯一的数据类型,所以对于Redis
的使用新手来说,这是非常自然的。
因为Redis
的key
是字符串,当我们使用字符串类型作为值的时候,我们是将一个字符串映射到另一个字符串。字符串类型在很多场景中是非常有用的,比如缓存HTML
片段或者页面。
接下来使用redis-cli
使用一下字符串类型(在这个文章中所有的示例都通过使用redis-cli
):
> set mykey somevalue
OK
> get mykey
"somevalue"
正如你看到的,使用SET
和GET
命令可以设置和获取一个字符串值。值得注意的是SET
将会覆盖key
已经存在的值,即使这个key
关联了一个不是字符串的值。所以SET
表现为一个任务。
值可以是任意类型的字符串(包括二进制数据),比如你可以存储jpeg
图片。一个值不能超过512MB
。
SET
命令有一些有趣的选项,作为而外的参数。比如。我可以让SET
在key
已经存在的时候失败,或者相反,只有在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
命令将字符串转化为integer
,自增1,然后保存成新的值,还有其他类似的命令,比如INCRBY
、DECR
、DECRBY
。在内部他们其实是一样的命令,只是执行的时候有一点小差别而已。
INCR
是原子的意味着什么?这意味着即使多个客户端发送INCR
获取同一个key,将不会进入竞争状态,例如,客户端1获取到10
,同时客户端2也获取到10
是不可能的,全部获取的都是11
,并且将11
保存成新的值。最懂的值将会是12
,读取-自增-设置
三个操作将在其他客户端还没执行命令的时候同时完成。
有很多的命令可以操作字符串。比如GETSET
命令给一个key
设置一个新的个值,同时返回旧的值作为结果。比如,你的系统在你的网站有一个新的访客到来的时候,使用INCR
自增一个Redis
的key
,你可以使用这个命令。你可能需要每小时收集所有的信息,甚至不错过每一次增长,你可以GETSET
一个key
,将它的值设置为0的同时获取新的值。
使用一个命令同时设置或者获取多个key
的能力是降低延迟的好方法。MSET
和MGET
命令可以做到:
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"
当MGET
使用的时候,Redis
将会返回一个值。
0x003 修改和查询key
空间
有一些命令在部分类型中并没有定义,但是和key
空间交互的时候是非常有用的,所以,可以使用在任意类型的key
之上。
比如,当DEL
命令删了吃了一个key和他所关联的值的时候,EXISTS
命令返回1
或者0
去标记一个key
是否存在在数据库,不管这个key
关联的值是什么类型。
> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0
从例子中可以看出,DEL
命令返回1
或者0
取决与key
是否被移除了(存在,或者没有这个名字的key
)。
From the examples you can also see how DEL itself returns 1 or 0 depending on whether the key was removed (it existed) or not (there was no such key with that name).
有很过key
空间相关的命令,上面的两个命令和TYPE
命令是最主要的,TYPE
命令返回的是存储在这个key
中的类型。
> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none
0x004 Redis
期限:key
的生存时间
在继续了解更多复杂的数据类型之前,我们需要先讨论另一个无视值类型的特性,我们称之为Redis
生存时间。简单来说你可以为一个key
设置一个过期时间,这个就是key
可以存在的时间。当可以存在的时间过了,这个key
就会自动销毁,就像用户使用DEL
命令删除了这个key。
关于Redis
期限的一些简单信息:
- 他们可以使用秒或者微妙作为单位
- 最小的单位是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)
在两次相隔5s的GET
调用中,key
完全消失了。在上面的例子中,我们用EXPIRE
去设置过期时间(当然也可以用来给一个已经存在过期时间的key
设置一个不同的过期时间,比如PERSIST
可以用来移除过期时间,使这个key
永久持久化)。当然我们也可以使用其他Redis
命令创建一个有过期时间的key
。比如,使用SET
命令的选项:
> set key 100 ex 10
OK
> ttl key
(integer) 9
的绗棉这个栗子设置了一个值为100,过期时间为10秒的key
,接下来的TTL
命令用来检查这个key
剩下的生存时间。
为了用毫秒设置和检查生存时间,可以使用PEXPIRE
和PTTL
命令,和完整的SET
命令选项。
Redis List
为了解释List
这种数据类型,最好先来点理论知识作为开胃菜,其实术语List
在信息技术领域的使用是经常是不恰单的。比如Python Lists
并不像名字所体现的(Linked Lists
),更像Arrays
(实际上相同的数据类型在Ruby
中称为Array
)。
从一般的观点看,一个List
只是一个由一系列有序元素组成的列表:10,20,1,2,3
。但是使用Array
实现的List
和用Linked List
实现的List
在特性上有很大的不同。Redis List
是通过Linked List
实现的。这意味着即使你有百万个元素在列表内,添加一个元素的操作到头部或者尾部的操作的时间是一个常量。使用LPUSH
命令添加一个新元素到一个有10个元素的列表的头部所耗费的时间是和添加一个元素到一个有10000000万元素的列表的头部是一样的。
不利的一面是什么呢?使用Arrays
实现的List
通过索引访问一个元素是非常迅速的(常量时间),然而用Linked List
实现的则不会这么快(这个操作需要的时间是和要访问的索引成正比的)。
Redis List
使用Linked List
实现是因为对于数据库系统来说,它需要能够通过非常快的方式添加元素到一个非常长的列表。接下来你将看到一个非常强的优势,那就是Redis Lists
可以采取常量长度在常量时间内。
当需要非常快的访问一个巨大聚合元素的其中一个数据时候,有另一种数据结构可供选择,那就是Sorted Sets
,Sorted Set
将在下面的章节涉及。
Redis Lists
使用第一步
LPUSH
命令添加一个新的元素到一个列表的左边(头部),RPUSH
命令添加一个新的元素到一个列表的右边(尾部)。LRAGE
命令从列表中提取元素。
> 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"
注意:LRAGE
需要两个索引,要返回的第一个元素的索引和最后一个元素的索引。两个所以都可以被导航,告诉Redis
从开始统计到结束:所以,-1是列表的最后一个元素,-2是列表的倒数第一个元素,以此类推。
就像你看到的RPISH
添加元素到列表的右边,LPUSH
添加元素到列表的左边。
As you can see RPUSH appended the elements on the right of the list, while the final LPUSH appended the element on the left.
两个命令都是可变参数长度的命令,,这意味着你可以在一次执行中自由的推入多个元素到一个列表:
> 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 Lists
中的一个重要操作是pop
的能力。弹出元素是从列表获取元素并且淘汰元素的操作。你可以从左边或则右边弹出元素,就像你可以从列表两边推入元素一样:
> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"
我们推入了三个元素并且弹出了三个元素,所以执行完这一系列命令,这个列表最终变成空的,并且将不再有数据弹出。如果我们依旧尝试弹出其他元素,我们将会得到如下结果:
> rpop mylist
(nil)
Redis returned a NULL value to signal that there are no elements in the list.
Redis Lists
的应用场景
Lists
对一系列任务都很有帮助,下面是两个典型应用场景:
- 在社交网络中记住用户最新更新的文章
- 进程间交流,使用生产-消费模式,生产者推入元素到列表中,消费者消费这些元素,并执行动作。
Redis
有特殊的列表命令去保证这种用户场景更加可靠和有效。
比如,Ruby
的库resque
和sidekiq
在底层使用Redis Lists
去实现后台任务。
流行的社交网络Twitter
将最新的Twitter
用户文章推入Redis Lists
。
为了一步一步概括一个普通的用户场景,想想你的主页显示了发布在一个照片分享社交网络的最新的照片,你想要很快的访问。
每次一个用户发布一张新的照片,我们使用LPUSH
将照片的ID放入一个列表。当用户访问主页的时候,我们使用LRANGE 0 9
去获取最新的10张照片。
有限List
在很多应用场景下,我们只是想使用列表去存储最新的项目,比如:社交网络更新,日志,诸如此类。Redis
允许我们像使用有限集合一样使用列表,只记住最新的N条数据并使用LTRIM
抛弃掉最旧的数据。
LTRIM
和LRANGE
很像,但是它不是现实指定的元素范围,而是设置指定的方位为新的列表值。所有不在这个范围之内的元素将被移除:
An example will make it more clear:
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
TRIM
命令告诉Redis
只获取列中中索引0到2的元素,其他不在这个范围内的元素全部抛弃。这让一个简单但是有用的模式得到实现:向列表推入数据操作+修剪操作一起,实现了添加一个新元素并抛弃超出范围的元素:
LPUSH mylist <some element>
LTRIM mylist 0 999
上面的命令结合起来实现了添加一个新的元素到列表并获取列表前1000条最新的元素。LRANGE
命令让你可以获取到定模的元素并且不许要记住每一个旧的数据
注意:尽管LRANGE
命令技术上是一个O(N)
的命令,获取列表头部或者尾部很小范围的的数据依旧是一个常量时间操作。
List
会阻塞的操作
列表有一个很特别的特性让它可以很适合用来实现队列,一般作为内部进程通信系统的构建块:阻塞操作。
想想你的一个进程想要将一个元素推入列表,另一个进程想要对这些元素进行某些操作。这就是通常说的生产者/消费者模式,可以用下面的方式简单的实现:
- 生产者使用
LPUSH
向列表推入数据。 - 消费者使用
RPOP
从列表消费数据。
然而,有时列表有可能时空的,没有什么好执行的,所以RPOP
将会返回NULL
,这种情况下,消费者强制等待一些时间然后重新
- 强制
Redis
和客户端去执行无效的命令(当列表是空的的时候,针对所有的请求其实没有做任何的工作,只是简单的返回NULL
)。 - 添加一个延迟去执行项目,因为一个工作进程接收到
NULL
后会等待一些时间。让延迟更小,我们在可以在两个执行RPOP
命令之间等待更少的时间,但是会引起问题1,也就是更多的无效请求。
所以Redis
实现了BRPOP
和BLPOP
命令,这个命苦可以让RPOP
和LPOP
可以在列表为空的时候堵塞:他们将只在一个新的元素添加进列表的时候执行,或者当用户指定的超时时间到了。
这是一个关于我们可以使用的BRPOP
命令示例:
> brpop tasks 5
1) "tasks"
2) "do_something"
这意味着:等待列表中的元素,但是如果5s之后没有元素就返回。
值得注意的是,你可以设置超时时间为0,从而让线程永远等待,当然你也可以指定多个列表,而不是一个,同一时间等待多个列表,将会收到第一个收到新元素列表的通知。
一些关于BRPOP
的笔记:
- 客户端在一种有序的方式下运行:第一个客户端堵塞等待一个列表,它将在其他客户端推入元素的时候第一个被服务,并以此类推。
- 返回值和
RPOP
不一样:是一个包含两个元素的数组,他包含了key
的名字,因为BRPOP
和BLPOP
可以做到堵塞等待多个列表的元素。 - 如果超时时间已经到了,将会返回
NULL
。
关于列表和堵塞操作还有更多的星系你需要知道。我们推荐你可以阅读下面的更多内容:
- 使用
RPOPLPUSH
命令可以构建一个更安全的队列或者旋转队列。 -
BRPOPLPUSH
命令是RPOPLPUSH
命令堵塞的变形。