Redis数据结构和操作以及优点和五种操作场景的介绍

redis不只是一个简单的键(key)-值(value)数据库,实际上它是一个数据结构服务器,支持各种类型的值。也就是说,在传统的键-值数据库中,你把字符串键与字符串值联系起来,而在redis,值不仅限于一个简单的字符串,还可以是更复杂的数据结构。下面列出了所有redis支持的数据结构,下文会分别对这些结构进行介绍:

  • 二进制安全字符串
  • 队列(lists):基于插入顺序有序存储的字符串元素集合。主要是链式的list。
  • 集(sets):元素唯一的、无序的字符串元素集合。
  • 有序集(sorted sets):与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客户端执行)。

> set mykey somevalue

OK

> get mykey

"somevalue"

  • 1
  • 2
  • 3
  • 4

正如你所看到的,GETSET命令用于设置或获取一个字符串值。需要注意的是,如果键已经存在,SET会覆盖它的值,即使与这个键相关联的不是字符串类型的值。SET相当于赋值。 
值可以是任意类型的字符串(包含二进制数据),你也可以使用一个jpeg图像。值在大小不能大于512MB。 
SET命令配上一些额外的参数,可以实现一些有趣的功能。例如,我可以要求如果键已经存在,SET就会失败,或者相反,键已经存在时SET才会成功。

> set mykey newval nx

(nil)

> set mykey newval xx

OK

  • 1
  • 2
  • 3
  • 4

虽然字符串是最基础的数据类型,你仍可以对它执行一些有趣的操作,比如原子性的自增:

> set counter 100

OK

> incr counter

(integer) 101

> incr counter

(integer) 102

> incrby counter 50

(integer) 152

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

INCR命令把字符串解析成一个整数,然后+1,把得到的结果作为一个新值存进去。还有其它相似的命令:INCRBYDECRDECRBY。从命令的实现原理上讲,这几个命令是相同的,只是有一点细微的差别。 
为什么说INCR是原子性的呢?因为即使是多个客户端对同一个键使用INCR命令,也不会形成竞争条件。举个例子,像这样的情况是不会发生的:客户端1读取到键是10,客户端2也读到键值是10,它们同时对它执行自增命令,最终得到的值是11。实际上,最终的得到的值是12,因为当一个客户端对键值做读-自增-写的过程中,其它的客户是不能同时执行这个过程的。 
有许多用于操作字符串的命令,例如GETSET命令,它给键设置一个新值,并返回旧值。比如你有一个系统,每当有一个新的访问者登陆你的网站时,使用INCR对一个键值自增。你可能想要统计每个小时的信息,却又不希望丢失每次自增操作。你可以使用GETSET命令,设置一个新值“0”,同时读取旧值。 
redis支持通过一条命令同时设置或读取多个键,这对于减少延时很有用。这就是MSET命令和MGET命令:

> mset a 10 b 20 c 30

OK

> mget a b c

1) "10"

2) "20"

3) "30"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用MGET时,redis返回包含多个值的数组。

更改或查询键空间

【2】 
有些命令并没有指定特定的类型,但在与键空间的交互有非常有用,因此可以用于任意类型的键。 
举个例子,EXISTS命令返回1或者0,用于表示某个给定的键在数据库中是否存在。DEL命令删除键以及它对应的值而不管是什么值。

> set mykey hello

OK

> exists mykey

(integer) 1

> del mykey

(integer) 1

> exists mykey

(integer) 0

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

DEL返回1还是0取决于键是(键存在)否(键不存在)被删除掉了。 
有许多键空间相关的命令,但以上这两个命令和TYPE命令是最基本的。TYPE命令的作用是返回这个键的值的类型。

> set mykey x

OK

> type mykey

string

> del mykey

(integer) 1

> type mykey

none

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

键的生命周期

在介绍更多更复杂的数据结构之间,我们先讨论另一个与值类型无关的特性,那就是redis的期限(redis expires)。最基本的,你可以给键设置一个超时时间,就是这个键的生存周期。当生存周期过去了,键会被自动销毁,就好像被用户执行过DEL一样。 
一些关于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)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

键在两次调用之间消失了,这是因为第二次调用的延迟了超过5秒的时间。在上面的例子中,我们使用EXPIRE命令设置生命周期(它也可以用于为一个已经设置过生命周期的键重新设置生命周期,PERSIST命令可以用于移除键的命令周期,使它能够长期存在)。我们还可以使用redis命令在创建键的同时设置生命周期。比如使用带参数的SET命令:

> set key 100 ex 10

OK

> ttl key

(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列表。 
快速地读取很大一堆元素的中间元素也是重要的,这时可以使用另一种数据结构,称为有序集(sorted sets)。本文后面会讲到有序集。

regis列表第一步

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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意,LRANGE命令需要输入两个下标,即范围的第一个元素下标和最后一个元素下标。两个下标都可以是负的,意思是从尾部开始数:因此-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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Redis中定义了一个重要的操作就是删除元素。删除命令可以同时从列表中检索和删除元素。你可以从左边或者右边删除元素,和从两边增加元素的方法类似:

> rpush mylist a b c

(integer) 3

> rpop mylist

"c"

> rpop mylist

"b"

> rpop mylist

"a"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们增加和删除的三个元素,因此最后列表是空的,没有元素可以删除。如果我们尝试继续删除元素,会得到这样的结果:

> rpop mylist

(nil)

  • 1
  • 2

redis返回空值说明列表中没有元素了。

列表的常见用例

列表可以用于完成多种任务,以下是两个非常有代表性的用例:

  • 记住用户发布到社交网络的最新更新。
  • 使用消费者-生产者模型进行进程间通信,生产生把表项(items)放进列表中,消费者(通常是工作者)消费这些items并执行一些行为。redis针对这种用例有一些特殊的列表命令,既可靠又高效。

例如非常有名的Ruby库resquesidekip,在底层都使用了Redis列表来实现后台作业。 
著名的社交网络Twitter使用Redis列表来获取用户发布的最新的消息。 
为了一步一步地描述一个常见用例,假设要在你的主页上展示社交网络上最新分享的照片并且加速访问。

  • 每当一个用户发布了一张新的照片,我们使用LPUSH命令把它的ID加入到列表中。
  • 当用户访问这个主页,我们使用LRANGE 0 9获取最新加入的10个表项。

限制列表

很多情况下我们只想要使用列表来存储最新的几条表项,例如社交网络更新、日志或者其它。 
Redis允许我们使用列表作为一个固定集合,使用LTRIM命令,只记录最新的N条记录,而丢弃所有更早的记录。 
LTRIM命令和LRANGE命令相似,但不像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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面的LTRIM命令告诉Redis只取得列表中下标为0到2的元素,其它的都要丢弃。这就是一种简单有用的模式成为了可能:列表增加(push)元素操作+列表提取(trim)元素操作=增加一个元素同时删除一个元素使得列表元素总数有限:

LPUSH mylist <some element>

LTRIM mylist 0 999

  • 1
  • 2

上面的操作结合增加一个元素但只是存在1000个最新的元素在列表中。通过LRANGE你可以获取最新的表项而不需要记住旧的数据。 
注意:由于理论上LRANGE是O(N)命令,读取从头开始或从尾开始的小范围数据是常数时间的操作。

列表中是阻塞型操作

列表的一些特性使它适合现实队列(queues),也通常作为进程间通信系统的一个基础组件:阻塞式操作。 
假设你通过一个进程把元素增加到列表中,使用另一个进程对这些元素做些实际的操作。这是通常的生产者/消费者基础,你可以用下面这种简单的方法实现:

  • 生产者调用LPUSH,把元素加入列表
  • 消费者调用RPOP,把元素从列表中取出或处理

有没有可能出现这种情况,列表是空的,没有什么东西可以处理,因此RPOP返回NULL。这种情况下,消费者不得不等一会再尝试RPOP。这就叫轮询。这并不是一个好方法,因为它有以下缺点:

  1. 要求redis和客户端执行没有意义的命令(当列表为空是所有的请求都不会执行实际工作,只是返回NULL)
  2. 工作者在收到NULL之后加入一个延时,让它等待一些时间。如果让延时小一点,在两次调用RPOP之间的等待时间会比较短,这成为第一个问题的放大-调用Redis更加没有意义

因此Redis实现了命令BRPOPBLPOP,它是RPOPLPOP的带阻塞功能的版本:当列表为空时,它们会等到一个新的元素加入到列表时,或者用户定义的等待时间到了时,才会返回。 
这是BRPOP调用的一个例子,我们可以在工作者进程使用它:

> brpop tasks 5

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举例:

> del mylist

(integer) 1

> lpush mylist 1 2 3

(integer) 3

  • 1
  • 2
  • 3
  • 4

然而,我们不能对一个已经存在的键执行与它类型不同的操作:

> set foo bar

OK

> lpush foo 1 2 3

(error) WRONGTYPE Operation against a key holding the wrong kind of value

> type foo

string

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

规则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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

当所有元素被取出,这个键就不存在了。 
规则3举例:

> del mylist

(integer) 0

> llen mylist

(integer) 0

> lpop mylist

(nil)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

redis中的哈希(hashed)

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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

hash表示对象(object)非常方便。实际上,可以放入一个hash的域的数量没有限制(不考虑可用内存),因此你可以在你的应用中用许多不同的方式使用哈希。 
HMSET命令为hash设置多个域,而HGET获取某一个域。HMGETHGET相似,但它返回由值组成的数组。

> hmget user:1000 username birthyear no-such-field

1) "antirez"

2) "1977"

3) (nil)

  • 1
  • 2
  • 3
  • 4

还有一些命令可以对单个的域执行操作,例如HINCRBY

> hincrby user:1000 birthyear 10

(integer) 1987

> hincrby user:1000 birthyear 10

(integer) 1997

  • 1
  • 2
  • 3
  • 4

你可以查看这篇文档《hash命令全列》 
把小的哈希(少量的元素,较小的值)用特殊的编码方式存放在内存中并不是什么难事,因此它们的空间效率非常高。

redis的集(sets)

Redis的集是字符串无序的集合。SADD向集中增加一些元素。对于集合还有很多其它的操作,例如测试某个给定的元素是否存在,多个集合之间求交集、合集或者差集,等。

> sadd myset 1 2 3

(integer) 3

> smembers myset

1. 3

2. 1

3. 2

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这个例子中,我向myset中增加了三个元素,并让redis返回所有的元素。正如你所看到的,它们是无序的。每次调用,redis都可能以任何顺序返回元素,因此在这里,用户不能对元素的顺序有要求。 
redis提供测试成员的命令。这个给定的元素是否存在?

> sismember myset 3

(integer) 1

> sismember myset 30

(integer) 0

  • 1
  • 2
  • 3
  • 4

“3”是这个集中的一员,而“30”不是。 
集善于表现对象之间的关系。例如我们可以很容易使用集实现标签(tags)。处理这个问题的一个简单的模型就是把所有要打标签的对象设置一个集。集包含相关对象的标签的ID。 
假设我们想要为新闻加标签。ID为1000的新闻被打上1,2,5和77这几个标签,我们可以用一个集将这些标签ID与新闻关联起来:

> sadd news:1000:tags 1 2 5 77

(integer) 4

  • 1
  • 2

然而有时我会想要相反的关系:列表中的所有新闻都被打上一个给定的标签:

> 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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

要获取一个对象的所有标签是很麻烦的。

> smembers news:1000:tags

1. 5

2. 1

3. 77

4. 2

  • 1
  • 2
  • 3
  • 4
  • 5

注意:在这个例子中我们假设你还有另一个数据结构,例如redis的哈希,用于标签ID到标签名的映射。 
如果使用正确的redis命令,可以通过并不繁琐却简单的方式去实现。例如我们可能想到同时拥有1,2,10和27标签的所有对象。我们可以使用SINTER命令执行不同集之间的求交运算。我们可以这么用:

> sinter tag:1:news tag:2:news tag:10:news tag:27:news

... results here ...

  • 1
  • 2

求交集运算不是唯一可以执行的操作,你还可以执行求并集运算、求差集运算、提取任意一个元素等。 
提取一个元素的命令是SOP,它对于模拟某些问题很方便。例如要实现一个基于网页的扑克牌游戏,你可能会把你的牌(deck)做成一个集。假设我们使用一个字符前缀来表示C(梅花)、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

  • 1
  • 2
  • 3
  • 4
  • 5

现在我们给每个玩家提供5张牌。SPOP命令移除一个随机的元素,并把它返回给客户端,因此在这个用例中是最好的操作。 
然后如果我们直接对deck调用它,下一轮游戏我们需要把再次填写所有的牌,这还不够理想。因此在开始之前,先把集中存储的deck键做一个备份到game中。使用SUNIONSTORE来实现,把结果存到另一个集中。这个命令通常是对多个集做求并集运行的。对一个集求并集运算就是它自己,因此可以用于复制:

> sunionstore game:1:deck deck

(integer) 52

  • 1
  • 2

现在我已经准备好为第一个玩家发五张牌了。

> 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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

一对J,不太好。。。 
现在是时候介绍集中元素个数的命令了。在集理论中,元素个数常被为集的势,因此这个命令是SCARD

> scard game:1:deck

(integer) 47

  • 1
  • 2

计算公式:52-5=47 
如果你只是想得到一个随机的元素但不把它从集中删除,SRANDMEMBER命令适合这个任务。它还可以提供返回重复元素或非重要元素的功能。

Redis的有序集

有序集像一种将集和哈希混合的数据类型。像集一样,有序集由唯一的不重复的字符串元素组成。因此某种意义上说,有序集也是一个集。 
集中的元素是无序的,而有序集中的元素都基于一个相关联的浮点值排序。这个浮点值称为分数(score)(每个元素都映射到一个值,因此和哈希相似)。 
此外,有序集中的元素是按顺序取的(它们不是按照要求排序的,排序是这个数据结构用于表现有序集的一个特性【4】)。它们按照下面的规则排序:

  • 假设A和B是分值不同的两个元素,如果A的分数>B的分数,则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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

正如你所见,ZADDSADD相似,但是需要一个额外的参数(位置在要加的元素之前),这就是分数。ZADD也是参数可变的,你可以随意地定义多个“分数-值”对,虽然上面的例子没有这么写。 
要求有序集中的黑客名单按他们的出生年份排序是没有意义的,因为它们已经是这样的了。 
实现细节:有序集是基于一个双端口数据结构实现的,包含一个跳跃表和一个哈希表。因此增加一个元素的执行时间是O(log(N))。这很好,当我们请求有序的元素时不需要其它的工作,它们已经是排序的了:

> 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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意:0和-1的意思是从下标为0的元素开始到最后一个元素(这里的-1和LRANGE命令中的-1一样)。 
如果想要反向排序,从最年轻到最老呢?使用ZREVERANGE代替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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

也可以同时返回分数,使用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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

基于范围的操作

有序集的功能强大远不止这些。它还可以基于范围操作。我们要取得所有出生年份早于(包括)1950年的人,就使用ZRANGEBYSCORE命令来实现:

> zrangebyscore hackers -inf 1950

1) "Alan Turing"

2) "Hedy Lamarr"

3) "Claude Shannon"

4) "Alan Kay"

5) "Anita Borg"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们请求Redis返回所有分数在无限到1950(两边都是闭区间)之间的元素。 
也可以移除某个范围内的元素。我们要从有序集中移除所有出生年份在1940和1960之间的黑客:

> zremrangebyscore hackers 1940 1960

(integer) 4

  • 1
  • 2

ZREMRANGEBYSCORE命令的名字也许不是很好,但是真的很有用,它返回被移除的元素的个数。 
另一个为有序集元素定义的非常有用的操作是获取排名(get-rank)操作。可以询问一个元素在它的有序集中的位置。

> zrank hackers "Anita Borg"

(integer) 4

  • 1
  • 2

ZREVRANK命令也可以获取排名,不过元素是逆序排序的。

字典序的分数

最近的Redis 2.8版本引入了一个新特性,假设有序集中所有元素的分数相同(使用C语言的memcmp函数来比较元素,这样保证每个redis实例都会返回相同的结果)的情况下,允许按照字典序获得范围。【5】 
针对字典序范围操作的主要命令是ZRANGEBYLEXZREVRANGEBYLEXZREMRANGEBYLEXZLEXCOUNT。 
举个例子,我们再次把所有著名黑客加入到列表中,但这一次所有元素的分数都是0:

> 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"

  • 1
  • 2
  • 3

基于有序集的排序规则,它们是字典序排序的:

> 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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们可以使用ZRANGEBYLEX命令请求字典序的范围:

> zrangebylex hackers [B [P

1) "Claude Shannon"

2) "Hedy Lamarr"

3) "Linus Torvalds"

  • 1
  • 2
  • 3
  • 4

范围是开区间还是闭区间都可以(由第一个字符决定),字符串正无穷和负无穷分别通过+和-来定义。更多信息请查看文档。 
这个特性很重要,它使得我们使用有序集作为一个通常索引。举个例子,如果你想要使用一个128位的无符号整数来为元素索引,你所要做的只是把元素加入到一个有序集并设置一个相同的分数(比如0)以及一个由128位数值组成的8字节前缀。由于数值是大端的,字典序的顺序(原始字节序)实际上是数字序的,你可以在128位空间请求范围,以前缀降序返回元素的值。 
如果你想查看这个特性更严谨的演示,请查看 Redis autocomplete demo.

更新分数:排行榜

这是在切换到下一个话题之前最后一个关于有序集的点。有序集的分数可以随时被更新。只需要对一个在有序集中已经存在的元素执行ZADD就可以在O(log(N))更新它的分数(和位置)。同样的,当会经常更新时使用有序集非常合适。 
这个特性的一个通常用例是排行榜。典型的应用是Facebook的一个游戏,你可以使用户基于它们的高分排序,增加获取排名的操作,在排行榜上显示前N个用户及用户排名(例:你是第4932好的分数)

位图

位图其实不是一个真正的数据类型,只是在这种字符串类型上有一系列基于位的操作。由于字符串是二进制安全的,最大长度是512MB,因此可以设置多达2^32种不同的位串。 
位操作分为两类,一种是针对某一个位的常数时间操作,如果设置某个位为1或0,或获取某个位的值。另一种是对所有位的操作,例如计算一个给定范围内被设置为1的位的个数(如人数统计)。 
位图一个最大的优势就是存储信息时非常少空间。例如一个系统里面每个用户用一个不同的递增的用户ID表示,可以记录40亿用户的某个信息(这个用户是否想要接收邮件)只需要512M的内存。 
通过SETBIT命令和GETBIT命令设置或获取位:

> setbit key 10 1

(integer) 1

> getbit key 10

(integer) 1

> getbit key 11

(integer) 0

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

SETBIT命令把第一个参数作为位的序号,第二个参数作为这个位要设置的值,只能是1或0。如果地址位大于当前字符串的长度,这个命令会自动扩充字符串。 
GETBIT只返回位于某个位置的值。超过长度的位(地址位大于字符串的长度)将必然得到0。 
这三个命令是对所有位的操作:

  1. BITOP:执行不同字符串之间的位操作。包括AND、OR、XOR和NOT。
  2. BITCOUNT:执行统计操作,返回位集中1的个数
  3. BITPOS:找到第一个被设置为0或1的位置

BITPOSBITCOUNT都可以接受位的地址范围作为参数,这样就不对对整个字符串做操作。下面是一个关于BITCOUNT的简单例子:

> setbit key 0 1

(integer) 0

> setbit key 100 1

(integer) 0

> bitcount key

(integer) 2

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

位图的通常用法:

  • 各种实时分析
  • 存储要求空间时间高效的与对象ID关联的二进制信息。

假如你想知道你的网页用户中每天访问最长的时间【6】,你从开始记录天数,你把你的网页公开化的第一天开始,计数为0,然后每当有用户用户网页,使用SETBIT设置一位。位的下标可以简单的认为是当前系统时间,减去第一天得到偏移,然后除以3600*24。 
使用这种方式,对于每一个用户,你有一个包含每天访问信息的字符串来表示。使用BITCOUNT命令可以得到某个用户访问网页的天数。而使用多个BITOPs调用,可能简单地获取和分析位图,就很容易地计算出longest streak。 
为了共享数据或避免对一个很大的键操作,bitmap可以分裂成多个键。要把一个位图分裂成多个键而不是全部设置到一个键里面去,一个比较麻烦的方法就是让每个键存储M位,键名为键的数量/M,第N个位在这个键里的地址是数的位置%M。

HyperLogLogs

HyperLogLogs是一个概率性的数据结构,用于计算特别的东西(技术上常用于估算一个集的势)。通过计算一个特别的表项需要使用相对于要计算的表项本身来说很大的内存,因为需要记住你已经见过的元素,以免重复计算。然后有一系列算法可以按精度使用内存:在redis的实现中,你最终得到一个标准错误的估计测量结果的可能性少于1%【7】。这个算法的神奇之处在于你再大需要相对于要计算的表项本身来说很大的内存空间,可能是只使用一个常数数量的空间。最坏情况下12K字节,如果元素少的话,这个空间会更小。 
Redis中的HLL,从技术上讲,它是一个完全不同的数据结构,但像字符串一样编码,因此你可以使用GET来串行化一个HLL,使用SET并行到服务器。 
从概念上讲,HLL的接口使用集来完成相同的工作。你会使用SADD把每一个观测值写入集中,使用SCARD来查询集中的元素个数。因为SADD不会重复添加一个已存在的元素,因此集中的元素是唯一的。 
但是你不能把一个表项真正地添加入一个HLL,因为数据结构只包含并不真正存在的元素的状态,API是一样的:

  • 每当你看见一个新的元素,使用PFADD计数增加
  • 每当你想要恢复当前的近似值,使用PFCOUNT。【8】

> pfadd hll a b c d

(integer) 1

> pfcount hll

(integer) 4

  • 1
  • 2
  • 3
  • 4

使用这种数据结构的一个例子统计在一个调整中用户每天执行的请求数。 
Redis还可以对HLL执行求并集操作,请查询完整文件获取更多信息。

其它显著的特性

关于redis的接口,还有其它一些重要的信息不能放在这个文档中,但是非常值得引起你的注意:

  • 可以递增地迭代键空间
  • 可以在服务器端运行LUA脚本获取潜在因素和带宽
  • redis还是一个发布-订阅型服务器

一、redis 数据结构使用场景

原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码。目前目标是吃透 redis 的数据结构。我们都知道,在 redis 中一共有5种数据结构,那每种数据结构的使用场景都是什么呢?

String——字符串
Hash——字典
List——列表
Set——集合
Sorted Set——有序集合

下面我们就来简单说明一下它们各自的使用场景:

1. String——字符串

String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字(当数字类型用 Long 可以表示的时候encoding 就是整型,其他都存储在 sdshdr 当做字符串)。使用 Strings 类型,可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受 Redis 的定时持久化(可以选择 RDB 模式或者 AOF 模式),操作日志及 Replication 等功能。除了提供与 Memcached 一样的 get、set、incr、decr 等操作外,Redis 还提供了下面一些操作:

复制代码代码如下:


1.LEN niushuai:O(1)获取字符串长度
2.APPEND niushuai redis:往字符串 append 内容,而且采用智能分配内存(每次2倍)
3.设置和获取字符串的某一段内容
4.设置及获取字符串的某一位(bit)
5.批量设置一系列字符串的内容
6.原子计数器
7.GETSET 命令的妙用,请于清空旧值的同时设置一个新值,配合原子计数器使用

2. Hash——字典

在 Memcached 中,我们经常将一些结构化的信息打包成 hashmap,在客户端序列化后存储为一个字符串的值(一般是 JSON 格式),比如用户的昵称、年龄、性别、积分等。这时候在需要修改其中某一项时,通常需要将字符串(JSON)取出来,然后进行反序列化,修改某一项的值,再序列化成字符串(JSON)存储回去。简单修改一个属性就干这么多事情,消耗必定是很大的,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而 Redis 的 Hash 结构可以使你像在数据库中 Update 一个属性一样只修改某一项属性值。

复制代码代码如下:


存储、读取、修改用户属性

3. List——列表

List 说白了就是链表(redis 使用双端链表实现的 List),相信学过数据结构知识的人都应该能理解其结构。使用 List 结构,我们可以轻松地实现最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一个应用就是消息队列,可以利用 List 的 *PUSH 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。Redis 还提供了操作 List 中某一段元素的 API,你可以直接查询,删除 List 中某一段的元素。

复制代码代码如下:


1.微博 TimeLine
2.消息队列

4. Set——集合

Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。因为 Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

1.共同好友、二度好友
2.利用唯一性,可以统计访问网站的所有独立 IP
3.好友推荐的时候,根据 tag 求交集,大于某个 threshold 就可以推荐

5. Sorted Set——有序集合

和Sets相比,Sorted Sets是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,比如一个存储全班同学成绩的 Sorted Sets,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务。让重要的任务优先执行。

1.带有权重的元素,比如一个游戏的用户得分排行榜
2.比较复杂的数据结构,一般用到的场景不算太多

二、redis 其他功能使用场景

1. 订阅-发布系统

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

2. 事务——Transactions

谁说 NoSQL 都不支持事务,虽然 Redis 的 Transactions 提供的并不是严格的 ACID 的事务(比如一串用 EXEC 提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个 Transactions 还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。Redis 还提供了一个 Watch 功能,你可以对一个 key 进行 Watch,然后再执行 Transactions,在这过程中,如果这个 Watched 的值进行了修改,那么这个 Transactions 会发现并拒绝执行。

 

 

 

 

1. 使用redis有哪些好处?

(1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

(2) 支持丰富数据类型,支持string,list,set,sorted set,hash

(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除

 

2. redis相比memcached有哪些优势?

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

 

3. redis常见性能问题和解决方案:

(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件

(2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次

(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内

(4) 尽量避免在压力很大的主库上增加从库

(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...

这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。

 

 

4. mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

 相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:

voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

no-enviction(驱逐):禁止驱逐数据

 

5. MemcacheRedis的区别都有哪些?

1)、存储方式

Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。

Redis有部份存在硬盘上,这样能保证数据的持久性。

2)、数据支持类型

Memcache对数据类型支持相对简单。

Redis有复杂的数据类型。

3)、使用底层模型不同

它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。

Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

4),value大小

redis最大可以达到1GB,而memcache只有1MB

 

 

6. Redis 常见的性能问题都有哪些?如何解决?

 

1).Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。

 

2).Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

 

3).Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

4). Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内


 

 

7, redis 最适合的场景

Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用Redis呢?

       如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:

     1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
     2 、Redis支持数据的备份,即master-slave模式的数据备份。
     3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

(1)、会话缓存(Session Cache)

最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?

幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。

(2)、全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。

再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端

此外,对WordPress的用户来说,Pantheon有一个非常好的插件  wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

(3)、队列

Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。

如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。

(4),排行榜/计数器

Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:

当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:

ZRANGE user_scores 0 10 WITHSCORES

Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。

(5)、发布/订阅

最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。

Redis提供的所有特性中,我感觉这个是喜欢的人最少的一个,虽然它为用户提供如果此多功能。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值