Redis指南——04 进阶(下)

4.4 消息通知

场景:粉丝要求给博客加入邮件订阅功能,这样当发布新文章后订阅博客的用户就可以收到通知邮件了。那个粉丝还着重强调了一下:“这个功能对不习惯使用RSS的用户很重要,希望能够加上!”

小白心想:“是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS 功能都没有。” 邮件订阅功能太好实现了,无非是在博客首页放一个文本框供访客输入自己的邮箱地址,提交后博客会将该地址存入Redis的一个集合类型键中(使用集合类型是为了保证同一邮箱地址不会存储多个)。每当发布新文章时,就向收集到的邮箱地址发送通知邮件。

想的简单,可是做出来后小白却发现了一个问题:输入邮箱地址提交后,页面需要很久时间才能载入完。

原来小白为了确保用户没有输入他人的邮箱,在提交之后程序会向用户输入的邮箱发送一封包含确认链接的邮件,只有用户单击这个链接后对应的邮箱地址才会被程序记录。可是由于发送邮件需要连接到一个远程的邮件发送服务器,网络好的情况下也得花上2秒左右的时间,赶上网络不好10秒都必能发完。所以每次用户提交邮箱后页面都要等待程序发送完邮件才能加载出来,而加载出来的页面上显示的内容只是提示用户查看自己的邮箱单击确认链 接。“完全可以等页面加载出来后再发送邮件,这样用户就不需要等了。”小白喃喃道。

4.4.1 任务队列

当页面需要进行如发送邮件、复杂数据运算等耗时较长的操作时会阻塞页面的渲染。为了避免用户等待太久,应该使用独立的线程来完成这类操作。不过一些编程语言或框架不易实现多线程,这时很容易就会想到通过其他进程来实现。就小白的例子来说,设想有一个进程能够完成发邮件的功能,那么在页面中只需要想办法通知这个进程向指定的地址发送邮件就可以了。

通知的过程可以借助任务队列来实现。任务队列顾名思义,就是“传递任务的队列”。与任务队列进行交互的实体有两类,一类是生产者(producer),一类是消费者(consumer)。生产者会将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读入任务信息并执行。

对于发邮件这个操作来说页面程序就是生产者,而发邮件的进程就是消费者。当需要发送邮件时,页面程序会将收件地址、邮件主题和邮件正文组装成一个任务后存入任务队列中。同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其从队列中取出并执行。由此实现了进程间的通信。

使用任务队列有如下好处。

(1)松耦合。生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。

(2)易于扩展消费者可以有多个,而且可以分布在不同的服务器中,如图4-1所示。借此可以轻易地降低单台服务器的负载。

            图4-1 可以有多个消费者分配任务队列中的任务

4.4.2 使用Redis实现任务队列

说到队列很自然就能想到Redis的列表类型,3.4.2节介绍了使用LPUSH和RPOP命令实现队列的概念。如果要实现任务队列,只需要让生产者将任务使用LPUSH命令加入到某个键中, 另一边让消费者不断地使用RPOP命令从该键中取出任务即可。

在例子中,完成发邮件的任务需要知道收件地址、邮件主题和邮件正文。所以生产者需要将这三个信息组成对象并序列化成字符串,然后将其加入到任务队列中。而消费者则循环从队列中拉取任务,就像如下伪代码:

#无限循环读取任务队列中的内容

loop

$task=RPOR queue

if $task

#如果任务队列中有任务则执行它

execute( $task)

else

#如果没有则等待1秒以免过于频繁地请求数据

wait 1 second

到此一个使用Redis实现的简单的任务队列就写好了。不过还有一点不完美的地方:当任务队列中没有任务时消费者每秒都会调用一次RPOP命令查看是否有新任务。如果可以实现一 旦有新任务加入任务队列就通知消费者就好了。其实借助 BRPOP 命令就可以实现这样的需 求。

BRPOP命令和RPOP命令相似,唯一的区别是当列表中没有元素时BRPOP命令会一直阻塞住连接,直到有新元素加入。如上段代码可改写为:

loop

#如果任务队列中没有新任务,BRPOP 命令会一直阻塞,不会执行execute()。

$task=BRPOP queue, 0

#返回值是一个数组(见下介绍),数组第二个元素是我们需要的任务。

execute( $task[1])

BRPOP命令接收两个参数,第一个是键名,第二个是超时时间,单位是秒。当超过了此时间仍然没有获得新元素的话就会返回nil。上例中超时时间为“0”,表示不限制等待的时间,即 如果没有新元素加入列表就会永远阻塞下去。

当获得一个元素后BRPOP命令返回两个值,分别是键名和元素值。为了测试BRPOP命令,我们可以打开两个redis-cli实例,在实例A中:

redis A>BRPOP queue 0

键入回车后实例1会处于阻塞状态,这时在实例B中向queue中加入一个元素:

redis B>LPUSH queue task

(integer) 1

在LPUSH命令执行后实例A马上就返回了结果:

1) "queue"

2) "task"

同时会发现queue中的元素已经被取走:

redis>LLEN queue

(integer) 0

除了BRPOP命令外,Redis还提供了BLPOP,和BRPOP的区别在与从队列取元素时BLPOP 会从队列左边取。具体可以参照LPOP理解,这里不再赘述。

4.4.3 优先级队列

前面说到了博客需要在发布文章的时候向每个订阅者发送邮件,这一步骤同样可以使用任务队列实现。由于要执行的任务和发送确认邮件一样,所以二者可以共用一个消费者。然而设想这样的情况:假设订阅博客的用户有1000人,那么当发布一篇新文章后博客就会向任务队列中添加1000个发送通知邮件的任务。如果每发一封邮件需要10秒,全部完成这 1000个任务就需要近3个小时。问题来了,假如这期间有新的用户想要订阅博客,当他提交完自己的邮箱并看到网页提示他查收确认邮件时,他并不知道向自己发送确认邮件的任务被加入到了已经有1000个任务的队列中。要收到确认邮件,他不得不等待近3个小时。多么糟糕的用户体验!而另一方面发布新文章后通知订阅用户的任务并不是很紧急,大多数用户并 不要求有新文章后马上就能收到通知邮件,甚至延迟一天的时间在很多情况下也是可以接受 的。

所以可以得出结论当发送确认邮件和发送通知邮件两种任务同时存在时,应该优先执行前者。为了实现这一目的,我们需要实现一个优先级队列。

BRPOP命令可以同时接收多个键,其完整的命令格式为BLPOP key [key …]timeout,如BLPOP queue:1 queue:2 0。意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素。例如,打开两个redis-cli实例,在实例A中:

redis A>BLPOP queue:1 queue:2 queue:3 0

在实例B中:

redis B>LPUSH queue:2 task

(integer) 1

则实例A中会返回:

1) "queue:2"

2) "task"

如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。我们先在queue:2和queue:3中各加入一个元素:

redis>LPUSH queue:2 task1

1) (integer) 1

redis>LPUSH queue:3 task2

2) (integer) 1

然后执行BRPOP命令:

redis>BRPOP queue:1 queue:2 queue:3 0

1) "queue:2"

2) "task1"

借此特性可以实现区分优先级的任务队列。我们分别使用queue:confirmation.email和queue:notification.email两个键存储发送确认邮件和发送通知邮件两种任务,然后将消费者的 代码改为:

loop

$task = BRPOP queue:confirmation.email,

queue:notification.email,

0

execute( $task[1])

这时一旦发送确认邮件的任务被加入到queue:confirmation.email队列中,无论queue: notification.email还有多少任务,消费者都会优先完成发送确认邮件的任务。

4.4.4 “发布/订阅”模式

除了实现任务队列外,Redis还提供了一组命令可以让开发者实现“发布/订阅”(publish/subscribe)模式。“发布/订阅”模式同样可以实现进程间的消息传递,其原理是这样 的: “发布/订阅”模式中包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。 发布者发布消息的命令是PUBLISH,用法是PUBLISH channel message,如向channel.1说一 声“hi”:

redis>PUBLISH channel.1 hi

(integer) 0

这样消息就发出去了。PUBLISH命令的返回值表示接收到这条消息的订阅者数量。因为此时没有客户端订阅channel.1,所以返回0。发出去的消息不会被持久化,也就是说当有客户 端订阅channel.1后只能收到后续发布到该频道的消息,之前发送的就收不到了。

订阅频道的命令是SUBSCRIBE,可以同时订阅多个频道,用法是SUBSCRIBE channel[channel …]。现在新开一个redis-cli实例A,用它来订阅channel.1:

redis A>SUBSCRIBE channel.1 Reading messages... (press Ctrl-C to quit)

1) "subscribe"

2) "channel.1"

3) (integer) 1

执行SUBSCRIBE命令后客户端会进入订阅状态,处于此状态下客户端不能使用除SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PUNSUBSCRIBE这4个属于“发布/订阅”模式的命令之外的命令(后面3个命令会在下面介绍),否则会报错。

进入订阅状态后客户端可能收到三种类型的回复。每种类型的回复都包含3个值,第一个 值是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。消息类型可能的取值有:

(1)Subscribe。表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个值是 当前客户端订阅的频道数量。

(2)message。这个类型的回复是我们最关心的,它表示接收到的消息。第二个值表示产生 消息的频道名称,第三个值是消息的内容。

(3)unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当 前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态,之后就可以执行其他非“发 布/订阅”模式的命令了。

上例中当实例A订阅了channel.1进入订阅状态后收到了一条subscribe类型的回复,这时我们打开另一个redis-cli实例B,并向channel.1发送一条消息:

redis B>PUBLISH channel.1 hi!

(integer) 1

返回值为1表示有一个客户端订阅了channel.1,此时实例A 收到了类型为message的回复:

1) "message"

2) "channel.1"

3) "hi!"

使用UNSUBSCRIBE命令可以取消订阅指定的频道,用法为UNSUBSCRIBE [channel[channel …]],如果不指定频道则会取消订阅所有频道① 。

注释:①由于redis-cli的限制我们无法在其中测试UNSUBSCRIBE命令。

4.4.5 按照规则订阅

除了可以使用SUBSCRIBE命令订阅指定名称的频道外,还可以使用PSUBSCRIBE命令订阅指定的规则。规则支持glob风格通配符格式(见3.1节),下面我们新打开一个redis-cli实例C 进行演示:

redis C>PSUBSCRIBE channel.?* Reading messages... (press Ctrl-C to quit)

1) "psubscribe"

2) "channel.?*"

3) (integer) 1

规则channel.?*可以匹配channel.1和channel.10,但不会匹配channel.。这时在实例B中发布消息:

redis B>PUBLISH channel.1 hi!

(integer) 2

返回结果为2是因为实例A和实例C两个客户端都订阅了channel.1频道。实例C接收到的回复是:

1) "pmessage"

2) "channel.?*"

3) "channel.1"

4) "hi!"

第一个值表示这条消息是通过PSUBSCRIBE命令订阅频道而收到的,第二个值表示订阅时使用的通配符,第三个值表示实际收到消息的频道命令,第四个值则是消息内容。

提示  使用PSUBSCRIBE命令可以重复订阅一个频道,如某客户端执行了 PSUBSCRIBE channel.? channel.?*,这时向channel.2发布消息后该客户端会收到两条消 息,而同时PUBLISH命令返回的值也是2而不是1。同样的,如果有另一个客户端执行了 SUBSCRIBE channel.10,和PSUBSCRIBE channel.?*的话,向channel.10发送命令该客户端 也会收到两条消息(但是是两种类型,message 和pmessage),同时PUBLISH命令会返回2。

PUNSUBSCRIBE命令可以退订指定的规则,用法是PUNSUBSCRIBE [pattern[pattern …]], 如果没有参数则会退订所有规则。

注意  使用PUNSUBSCRIBE命令只能退订通过PSUBSCRIBE命令订阅的规则,不会影响直接通过SUBSCRIBE命令订阅的频道;同样UNSUBSCRIBE命令也不会影响通过 PSUBSCRIBE命令订阅的规则。另外容易出错的一点是使用PUNSUBSCRIBE命令退订某 个规则时不会将其中的通配符展开,而是进行严格的字符串匹配,所以PUNSUBSCRIBE* 无法退订channel.*规则,而是必须使用PUNSUBSCRIBE channel.*才能退订。

4.5 管道

客户端和Redis使用TCP协议连接。不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延。根据网络性能 不同,往返时延也不同,大致来说到本地回环地址(loop backaddress)的往返时延在数量级上 相当于Redis处理一条简单命令(如LPUSH list 1 2 3)的时间。如果执行较多的命令,每个命令 的往返时延累加起来对性能还是有一定影响的。

在执行多个命令时每条命令都需要等待上一条命令执行完(即收到Redis的返回结果)才 能执行,即使命令不需要上一条命令的执行结果。如要获得post:1、post:2和post:3这3个键中的 title字段,需要执行三条命令,如图4-2所示。

           图4-2 不使用管道时的命令执行示意图(纵向表示时间)

Redis的底层通信协议对管道(pipelining)提供了支持。通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中每条命令都不依赖于之前命令的执行结果时就可以将这组命令一起通过管道发出。管道通过减少客户端与Redis的通信次数来实现降低往 返时延累计值的目的,如图4-3所示。

图4-3 使用管道时的命令执行示意图

4.6 节省空间

Jim Gray曾经说过:“内存是新的硬盘,硬盘是新的磁带。”内存的容量越来越大,价格也越来越便宜。2012年年底,亚马逊宣布即将发布一个拥有240GB内存的EC2实例,如果放到若 干年前来看,这个容量就算是对于硬盘来说也是很大的了。即便如此,相比于硬盘而言,内存 在今天仍然显得比较昂贵。而Redis是一个基于内存的数据库,所有的数据都存储在内存中,所以如何优化存储,减少内存空间占用对成本控制来说是一个非常重要的话题。

注释:Jim Gray是1998年的图灵奖得主,在数据库(尤其是事务)方面做出过卓越的贡献。其于2007年独自驾船在海上失踪。

4.6.1 精简键名和键值

精简键名和键值是最直观的减少内存占用的方式,如将键名very.important.person:20改成 VIP:20。当然精简键名一定要把握好尺度,不能单纯为了节约空间而使用不易理解的键名(比如将VIP:20修改为V:20,这样既不易维护,还容易造成命名沖突)。又比如一个存储用户性别 的字符串类型键的取值是male和female,我们可以将其修改成m和f来为每条记录节约几个字 节的空间(更好的方法是使用0和1来表示性别,稍后会详细介绍原因)① 。 注释:①3.2.4节还介绍过使用字符串类型的位操作来存储性别,更加节约空间。

4.6.2 内部编码优化

有时候仅凭精简键名和键值所减少的空间并不足以满足需求,这时就需要根据Redis内部编码规则来节省更多的空间。Redis为每种数据类型都提供了两种内部编码方式,以散列类型为例,散列类型是通过散列表实现的,这样就可以实现0(1)时间复杂度的查找、赋值操作,然 而当键中元素很少的时候,0(1)的操作并不会比0(n)有明显的性能提高,所以这种情况下Redis 会采用一种更为紧凑但性能稍差(获取元素的时间复杂度为0(n))的内部编码方式。内部编码 方式的选择对于开发者来说是透明的,Redis会根据实际情况自动调整。当键中元素变多时 Redis会自动将该键的内部编码方式转换成散列表。如果想查看一个键的内部编码方式可以使 用OBJECT ENCODING命令,例如:

redis>SET foo bar

OK

redis>OBJECT ENCODING foo

"raw"

Redis的每个键值都是使用一个redisObject结构体保存的,redisObject的定义如下:

typedef struct redisObject {

unsigned type:4;

unsigned notused:2; /* Not used */

unsigned encoding:4;

unsigned lru:22; /* lru time (relative to server.lruclock) */

int refcount;

void *ptr;

}robj;

其中type字段表示的是键值的数据类型,取值可以是如下内容:

#define REDIS_STRING 0

#define REDIS_LIST 1

#define REDIS_SET 2

#define REDIS_ZSET 3

#define REDIS_HASH 4

encoding字段表示的就是Redis键值的内部编码方式,取值可以是:

#define REDIS_ENCODING_RAW 0 /* Raw representation */

#define REDIS_ENCODING_INT 1 ed as integer */

#define REDIS_ENCODING_HT 2 /* Encoded as hash table */

#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */

#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */

#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */

#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */

#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */

各个数据类型可能采用的内部编码方式以及相应的OBJECT ENCODING命令执行结果如表4-2所示。

表4-2  每个数据类型都可能采用两种内部编码方式之一来存储

数据类型

内部编码方式

OBJECT ENCODING命令结果

字符串类型

REDIS_ENCODING_RAW

REDIS_ENCODING_INT

“raw”

“int”

散列类型

REDIS_ENCODING_HT

REDIS_ENCODING_ZIPLIST

“hashtable”

“ziplist”

列表类型

REDIS_ENCODING_LINKEDLIST

REDIS_ENCODING_ZIPLIST

“linkedlist”

“ziplist”

集合类型

REDIS_ENCODING_HT

REDIS_ENCODING_INTSET

“hashtable”

“intset”

有序集合类型

REDIS_ENCODING_SKIPLIST

REDIS_ENCODING_ZIPLIST

“skiplist”

“ziplist”

下面针对每种数据类型分别介绍其内部编码规则及优化方式。

1.字符串类型

Redis使用一个sdshdr类型的变量来存储字符串,而redisObject的ptr字段指向的是该变量 的地址。sdshdr的定义如下:

struct sdshdr {

int len;

int free;

char buf[];

};

其中len字段表示的是字符串的长度,free字段表示buf中的剩余空间,而buf字段存储的才 是字符串的内容。

所以当执行SET key foobar时,存储键值需要占用的空间是 sizeof(redisObject)+sizeof(sdshdr)+strlen("foobar")=30字节① ,如图4-4所示。 注释:①本节所说的字节数以64位Linux系统为前提。

图4-4 字符串键值“foobar”的存储结构

而当键值内容可以用一个64位有符号整数表示时,Redis会将键值转换成long类型来存储。如SET key 123456,实际占用的空间是sizeof(redisObject)=16字节,比存储"foobar"节省了一半的存储空间,如图4-5所示。

图4-5 字符串键值“123456”的内存结构

redisObject中的refcount字段存储的是该键值被引用数量,即一个键值可以被多个键引用。Redis启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享 对象,如果要设置的字符串键值在这10000个数字内(如SET key1 123)则可以直接引用共享对 象而不用再建立一个redisObject了,也就是说存储键值占用的空间是0字节,如图4-6所示。

图4-6 当执行了SET key1 123和SET key2 123后,key1和key2两个键都直接引用了一个已经建立好的共享对象,节省了存储空间

由此可见,使用字符串类型键存储对象ID这种小数字是非常节省存储空间的,Redis只需 存储键名和一个对共享对象的引用即可。

提示  当通过配置文件参数maxmemory设置了Redis可用的最大空间大小时,Redis不 会使用共享对象,因为对于每一个键值都需要使用一个redisObject来记录其LRU信息。

2.散列类型

散列类型的内部编码方式可能是REDIS_ENCODING_HT或 REDIS_ENCODING_ZIPLIST① 。在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式 编码散列类型的时机:

注释:①在Redis 2.4及以前的版本中散列类型的键采用REDIS_ENCODING_HT或 REDIS_ENCODING_ZIPMAP的编码方式。

hash-max-ziplist-entries 512

hash-max-ziplist-value 64

当散列类型键的字段个数少于hash-max-ziplist-entries参数值且每个字段名和字段值的长 度都小于hash-max-ziplist-value参数值(单位为字节)时,Redis就会使用REDIS_ ENCODING_ZIPLIST来存储该键,否则就会使用REDIS_ENCODING_HT。转换过程是透明的, 每当键值变更后Redis都会自动判断是否满足条件来完成转换。

REDIS_ENCODING_HT编码即散列表,可以实现O(1)时间复杂度的赋值取值等操作,其字段和字段值都是使用redisObject存储的,所以前面讲到的字符串类型键值的优化方法同样 适用于散列类型键的字段和字段值。

提示  Redis的键值对存储也是通过散列表实现的,与REDIS_ENCODING_HT编码方式类似,但键名并非使用redisObject存储,所以键名“123456”并不会比“abcdef”占用更少 的空间。之所以不对键名进行优化是因为绝大多数情况下键名都不会是纯数字。

补充知识  Redis支持多数据库,每个数据库中的数据都是通过结构体redisDb存储的。 redisDb的定义如下:

typedef struct redisDb {

dict *dict; /* The keyspace for this DB */

dict *expires; /* Timeout of keys with a timeout set */

dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */

dict *ready_keys; /* Blocked keys that received a PUSH */

dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */

int id;

} redisDb;

dict类型就是散列表结构,expires存储的是数据的过期时间。当Redis启动时会根据配 置文件中databases参数指定的数量创建若干个redisDb类型变量存储不同数据库中的数据。

REDIS_ENCODING_ZIPLIST编码类型是一种紧凑的编码格式,它牺牲了部分读取性能以 换取极高的空间利用率,适合在元素较少时使用。该编码类型同样还在列表类型和有序集合 类型中使用。REDIS_ENCODING_ZIPLIST编码结构如图4-7所示,其中zlbytes是uint32_t类型,表示整个结构占用的空间。zltail也是uint32_t类型,表示到最后一个元素的偏移,记录zltail使得 程序可以直接定位到尾部元素而无需遍历整个结构,执行从尾部弹出(对列表类型而言)等操 作时速度更快。zllen是uint16_t类型,存储的是元素的数量。zlend是一个单字节标识,标记结构 的末尾,值永远是255。

图4-7 REDIS_ENCODING_ZIPLIST编码的内存结构

在REDIS_ENCODING_ZIPLIST 中每个元素由4个部分组成。

第一个部分用来存储前一个元素的大小以实现倒序查找,当前一个元素的大小小于254字 节时第一个部分占用1个字节,否则会占用5个字节。

第二、三个部分分别是元素的编码类型和元素的大小,当元素的大小小于或等于63个字 节时,元素的编码类型是ZIP_STR_06B(即0<<6),同时第三个部分用6个二进制位来记录元 素的长度,所以第二、三个部分总占用空间是1字节。当元素的大小大于63且小于或等于16383 字节时,第二、三个部分总占用空间是2字节。当元素的大小大于16383字节时,第二、三个部 分总占用空间是5字节。

第四个部分是元素的实际内容,如果元素可以转换成数字的话Redis会使用相应的数字类 型来存储以节省空间,并用第二、三个部分来表示数字的类型(int16_t、int32_t等)。

使用REDIS_ENCODING_ZIPLIST编码存储散列类型时元素的排列方式是:元素1存储字 段1,元素2存储字段值1,依次类推,如图4-8所示。

图4-8 使用REDIS_ENCODING_ZIPLIST编码存储散列类型的内存结构

例如,当执行命令HSET hkey foo bar命令后,hkey键值的内存结构如图4-9所示。

图4-9 hkey键值的内存结构

下次需要执行HSET hkey foo anothervalue时Redis需要从头开始找到值为foo的元素(查找 时每次都会跳过一个元素以保证只查找字段名),找到后删除其下一个元素,并将新值 anothervalue插入。删除和插入都需要移动后面的内存数据,而且查找操作也需要遍历才能完 成,可想而知当散列键中数据多时性能将很低,所以不宜将hash-max-ziplist-entries和hash-maxziplist-value两个参数设置得很大。

3.列表类型

列表类型的内部编码方式可能是REDIS_ENCODING_LINKEDLIST或REDIS ENCODINGZIPLIST。同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码 的时机:

list-max-ziplist-entries 512

list-max-ziplist-value 64

具体转换方式和散列类型一样,这里不再赘述。 REDIS_ENCODING_LINKEDLIST编码方式即双向链表,链表中的每个元素是用 redisObject存储的,所以此种编码方式下元素值的优化方法与字符串类型的键值相同。 而使用REDIS_ENCODING_ZIPLIST编码方式时具体的表现和散列类型一样,由于REDIS_ENCODING_ZIPLIST编码方式同样支持倒序访问,所以采用此种编码方式时获取两端 的数据依然较快。

4.集合类型

集合类型的内部编码方式可能是REDIS_ENCODING_HT或REDIS_ENCODING_INTSET。当集合中的所有元素都是整数且元素的个数小于配置文件中的set-max-intset-entries参数指定 值(默认是512)时Redis会使用REDIS_ENCODING_INTSET编码存储该集合,否则会使用 REDIS_ENCODING_HT来存储。

REDIS_ENCODING_INTSET编码存储结构体intset的定义是:

typedef struct intset {

uint32_t encoding;

uint32_t length;

int8_t contents[];

} intset;

其中contents存储的就是集合中的元素值,根据encoding的不同,每个元素占用的字节大小 不同。默认的encoding是INTSET_ENC_INT16(即2个字节),当新增加的整数元素无法使用2个 字节表示时,Redis会将该集合的encoding升级为INTSET_ENC_INT32(即4个字节)并调整之前 所有元素的位置和长度,同样集合的encoding还可升级为INTSET_ENC_INT64(即8个字节)。

REDIS_ENCODING_INTSET编码以有序的方式存储元素(所以使用SMEMBERS命令获得 的结果是有序的),使得可以使用二分算法查找元素。然而无论是添加还是删除元素,Redis都 需要调整后面元素的内存位置,所以当集合中的元素太多时性能较差。

当新增加的元素不是整数或集合中的元素数量超过了set-max-intset-entries参数指定值 时,Redis会自动将该集合的存储结构转换成REDIS_ENCODING_HT。

注意  当集合的存储结构转換成REDIS_ENCODING_HT后,即使将集合中的所有非 整数元素删除,Redis也不会自动将存储结构转換回REDIS_ENCODING_INTSET。因为如 果要支持自动回转,就意味着Redis在每次删除元素时都需要遍历集合中的键来判断是否 可以转換回原来的编码,这会使得删除元素变成了时间复杂度为0(n)的操作。

5.有序集合类型

有序集合类型的内部编码方式可能是REDIS_ENCODING_SKIPLIST或 REDIS_ENCODING_ZIPLIST。同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方 式编码的时机:

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

具体规则和散列类型及列表类型一样,不再赘述。 当编码方式是REDIS_ENCODING_SKIPLIST时,Redis使用散列表和跳跃列表(skiplist)两 种数据结构来存储有序集合类型键值,其中散列表用来存储元素值与元素分数的映射关系以 实现0(1)时间复杂度的ZSCORE等命令。跳跃列表用来存储元素的分数及其到元素值的映射以 实现排序的功能。Redis对跳跃列表的实现进行了几点修改,其中包括允许跳跃列表中的元素 (即分数)相同,还有为跳跃链表每个节点增加了指向前一个元素的指针以实现倒序查找。

采用此种编码方式时,元素值是使用redisObject存储的,所以可以使用字符串类型键值的优化方式优化元素值,而元素的分数是使用double类型存储的。

使用REDIS_ENCODING_ZIPLIST编码时有序集合存储的方式按照“元素1的值,元素1的 分数,元素2的值,元素2的分数”的顺序排列,并且分数是有序的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AllenGd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值