Redis深入系列-0x014:Redis数据类型和概念介绍(上)

0x000 概述

Redis不是一个简单键值对存储器,而是一个数据结构服务,它支持不同类型的值。这意味着传统的键值对存储器将字符串键和字符串值关联起来,在Redis中,值的类型不仅仅局限于字符串,还可以是更加复杂的数据结构,下面是Redis支持的数据结构,将会在各个章节接触到:

  • 字节安全的字符串
  • 列表:根据插入顺序排序的字符串集合元素,是最基本的链表
  • 集合:唯一的无序的字符串元素
  • 有序集合:和集合很想但是每个字符串元素都关联着一个浮点型数字作为值,叫做分数。他总是按照分数排序,所以它不想集合那样,获取一个范围之内的元素。
  • 哈希:是一个由键值对关联起来的map,键和值都是字符串。对于RubyPython非常的友好。
  • 比特数组:使用特殊的命令可以向处理比特数组一样处理字符串,你可以设置或者清除独立的比特,将所有的比特设置为1,找到第一个比特等等等
  • HyperLogLogs:不解释。

知道这些数据类型和怎样使用对于解决命令索引给出的问题并不总是微不足道的。知道这些数据类型和怎样使用对于解决命令索引给出的问题是很重要的,所以这个文档将作为了解Redis数据类型和他们基本模式的一个入门课程。

对于所有的案例我们将使用redis-cli工具,这是一个很简单但是很便利的命令行工具,用来和Redis服务端做交互。

0x001 Rediskey

Rediskey是比特安全的,这意味着你可以使用任何的二进制序列作为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字符串类型是Rediskey可以关联的的最简单的数据类型。这是Mmcached唯一的数据类型,所以对于Redis的使用新手来说,这是非常自然的。

因为Rediskey是字符串,当我们使用字符串类型作为值的时候,我们是将一个字符串映射到另一个字符串。字符串类型在很多场景中是非常有用的,比如缓存HTML片段或者页面。

接下来使用redis-cli使用一下字符串类型(在这个文章中所有的示例都通过使用redis-cli):

> set mykey somevalue
OK
> get mykey
"somevalue"

正如你看到的,使用SETGET命令可以设置和获取一个字符串值。值得注意的是SET将会覆盖key已经存在的值,即使这个key关联了一个不是字符串的值。所以SET表现为一个任务。

值可以是任意类型的字符串(包括二进制数据),比如你可以存储jpeg图片。一个值不能超过512MB

SET命令有一些有趣的选项,作为而外的参数。比如。我可以让SETkey已经存在的时候失败,或者相反,只有在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,然后保存成新的值,还有其他类似的命令,比如INCRBYDECRDECRBY。在内部他们其实是一样的命令,只是执行的时候有一点小差别而已。

INCR是原子的意味着什么?这意味着即使多个客户端发送INCR获取同一个key,将不会进入竞争状态,例如,客户端1获取到10,同时客户端2也获取到10是不可能的,全部获取的都是11,并且将11保存成新的值。最懂的值将会是12读取-自增-设置三个操作将在其他客户端还没执行命令的时候同时完成。

有很多的命令可以操作字符串。比如GETSET命令给一个key设置一个新的个值,同时返回旧的值作为结果。比如,你的系统在你的网站有一个新的访客到来的时候,使用INCR自增一个Rediskey,你可以使用这个命令。你可能需要每小时收集所有的信息,甚至不错过每一次增长,你可以GETSET一个key,将它的值设置为0的同时获取新的值。

使用一个命令同时设置或者获取多个key的能力是降低延迟的好方法。MSETMGET命令可以做到:

> 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剩下的生存时间。

为了用毫秒设置和检查生存时间,可以使用PEXPIREPTTL命令,和完整的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 SetsSorted 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的库resquesidekiq在底层使用Redis Lists去实现后台任务。
流行的社交网络Twitter将最新的Twitter用户文章推入Redis Lists

为了一步一步概括一个普通的用户场景,想想你的主页显示了发布在一个照片分享社交网络的最新的照片,你想要很快的访问。

每次一个用户发布一张新的照片,我们使用LPUSH将照片的ID放入一个列表。当用户访问主页的时候,我们使用LRANGE 0 9去获取最新的10张照片。

有限List

在很多应用场景下,我们只是想使用列表去存储最新的项目,比如:社交网络更新,日志,诸如此类。
Redis允许我们像使用有限集合一样使用列表,只记住最新的N条数据并使用LTRIM抛弃掉最旧的数据。

LTRIMLRANGE很像,但是它不是现实指定的元素范围,而是设置指定的方位为新的列表值。所有不在这个范围之内的元素将被移除:
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实现了BRPOPBLPOP命令,这个命苦可以让RPOPLPOP可以在列表为空的时候堵塞:他们将只在一个新的元素添加进列表的时候执行,或者当用户指定的超时时间到了。
这是一个关于我们可以使用的BRPOP命令示例:

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

这意味着:等待列表中的元素,但是如果5s之后没有元素就返回。
值得注意的是,你可以设置超时时间为0,从而让线程永远等待,当然你也可以指定多个列表,而不是一个,同一时间等待多个列表,将会收到第一个收到新元素列表的通知。
一些关于BRPOP的笔记:

  • 客户端在一种有序的方式下运行:第一个客户端堵塞等待一个列表,它将在其他客户端推入元素的时候第一个被服务,并以此类推。
  • 返回值和RPOP不一样:是一个包含两个元素的数组,他包含了key的名字,因为BRPOPBLPOP可以做到堵塞等待多个列表的元素。
  • 如果超时时间已经到了,将会返回NULL

关于列表和堵塞操作还有更多的星系你需要知道。我们推荐你可以阅读下面的更多内容:

  • 使用RPOPLPUSH命令可以构建一个更安全的队列或者旋转队列。
  • BRPOPLPUSH命令是RPOPLPUSH命令堵塞的变形。
阅读 2k

推荐阅读

哎,好像不能申请多个专栏呢,原本这个专栏只放前端文章,现在看来不行了!就都放吧!

22 人关注
111 篇文章
专栏主页