来源:未知 时间:2015-07-08 09:40 作者:xxadmin 阅读:次
[导读] 前言 项目中用到了redis,但用到的都是最最基本的功能,比如简单的slave机制,数据结构只使用了字符串。但是一直听说redis是一个很牛的开源项目,很多公司都在用。于是我就比较奇怪...
前言 项目中用到了redis,但用到的都是最最基本的功能,比如简单的slave机制,数据结构只使用了字符串。但是一直听说redis是一个很牛的开源项目,很多公司都在用。于是我就比较奇怪,这玩意不就和 memcache 差不多吗?仅仅是因为memcache是内存级别的,没有持久化功能。而redis支持持久化?难道这就是它的必杀技? 带着这个疑问,我在网上搜了一圈。发现有个叫做huangz的程序员针对redis写了一本书叫做《redis设计与实现》,而且业界良心搞了一个reids2.6版本的注释版源码。这本书不到200页,估计2个星期能看完吧,之后打算再看下感兴趣部分的源码。当然,如果你不知道redis是干嘛的,请自行谷歌,简单说就是Key-Value数据库,而且 value 支持5种数据结构:
下面我们就从 redis 的内部结构开始说起吧:) 一、redis内部数据结构 首先需要知道,redis是用C写的。众所周知,任何系统对于字符串的操作都是最频繁的,而恰巧C语言的字符串备受诟病。然后作者就封装了一下 C 语言的字符串 char *。 总之,根据redis的业务场景,整个redis系统的底层数据支撑被设计为如下几种:
下面我们就分别来说说这4种数据结构。 1. 简单动态字符串sds
这个一看名字就能知道个大概了,因为字符串操作无非是增删查改,如果使用char[]数组,那是要死人的,任何操作都是O(N)复杂度。所以,要对某些频繁的操作实现O(1)级性能。但是我们还是得思考:
因为redis是一个key-value类型的数据库,而key全部都是字符串,value可以是集合、hash、list等等。同时,在redis的各种操作中,都会频繁使用字符串的长度和append操作,对于char[]来说,长度操作是O(N)的,append会引起N次realloc。而且因为redis分为client和server,传输的协议内容必须是二进制安全的,而C的字符串必须保证是\0结尾,所以在这两点的基础上开发sds 知道了上面几点就可以看下实现了,其实实现特别简单。它通过一个结构体来代表字符串对象,内部有个len属性记录长度,有个free用于以后的append操作,具体的值还是一个char[]。长度就不说了,只在插入的时候用一下,以后只需要维护len就可以O(1)拿到;对于free也很简单,vector不也是这么实现的嘛。就是按照某个阈值进行翻倍叠加。 2. 双端链表
这玩意当时刷数据结构与算法分析那本书看过,但是没怎么用到过。说白了双端链表就是有2个指针,一个指向链表头,一个指向链表尾。对每个节点而言,记录自己的父节点和子节点,这样双向移动速度会快很多。 还是老问题:
在Java或者C++中,都有现成的容器供我们使用,但是C没有。于是作者自己造了一个双端链表数据结构。而这个也是redis列表数据结构的基础之一(另外一个还是压缩列表)。而且双端链表也是一个通用的数据结构被其他功能调用,比如事务。 至于实现也是比较简单,双端链表,肯定有2个指针指向链表头和链表尾,然后内部维护一个len保存节点的数目,这样当使用LLEN的时候就能达到O(1)复杂度了。其他的,额,对每个节点而言,都有双向的指针,另外还有针对双端链表的迭代器,也是两个方向。 3. 字典(其实说Map更通俗)
这个虽然说经常用,但是对于redis来说确实是重中之重。毕竟redis就是一个key-value的数据库,而key被称为键空间(key space),这个键空间就是由字典实现的。第二个用途就是用作hash类型的其中一种底层实现。下面分别来说明。
对于字典的实现这里简单说明一下即可,因为很简单。
下面讲一下rehash的触发条件: 当新插入一个键值对的时候,根据used/size得到一个比例,如果这个比例超过阈值,就自动触发rehash过程。rehash分为两种:
思考一下,为什么需要两种rehash呢?答案还是为了性能,不过这点考虑的是redis服务的整体性能。当redis使用后台子进程对字典进行rehash的时候,为了最大化利用系统的copy on write机制,子进程会暂时将自然rehash关闭,这就是dict_can_resize的作用。当持久化任务完成后,将dict_can_resize设为true,就可以继续进行自然rehash;但是考虑另外一种情况,当现有字典的碰撞率太高了,size是指针数组的大小,used是hash表节点数量,那么就必须马上进行rehash防止再插入的值继续碰撞,这将浪费很长时间。所以超过dict_force_resize_ratio后,无论在进行什么操作,都必须进行rehash。 rehash过程很简单,分为3步:
同样是为了性能(当用户对一个很大的字典插入时候,你不能让系统阻塞来完成整个字典的rehash。所以redis采用了渐进式rehash。说白了就是分步进行rehash。具体由下面2个函数完成:
上面讲完了rehash过程,但是以前在组内分享redis的时候遇到过一个问题:
redis采用的策略是rehash过程中ht[0]只减不增,所以增加肯定是ht[1],查找、修改、删除则会同时在ht[0]和ht[1]进行。 Tips: redis为了减少存储空间,rehash还有一个特性是缩减空间,当多次进行删除操作后,如果used/size的比例小于一个阈值(现在是10%),那么就会触发缩减空间rehash,过程和增加空间类似,不详述了。 3. 跳跃表
redis使用了跳跃表,但是我发现。。。。我竟然不知道跳跃表是什么东东。亏我还觉得数据结构基础还凑合呢= =。于是赶紧去看了《数据结构与算法分析》,算是知道是啥玩意的。说白了,就是链表+二分查找的结合体。这里主要是研究redis的,所以就不细谈这个数据结构了。 和双端链表、字典不同的是,跳跃表在reids中不是广泛使用的,它在redis中的唯一作用就是实现有序集数据类型。所以等到集合的时候再深入了解。 二上一章我们介绍了redis的内部结构: 但是,创建这些完整的数据结构是比较耗费内存的,如果对于一个特别简单的元素,使用这些数据结构无异于大材小用。为了解决这个问题,redis在条件允许的情况下,会使用内存映射数据结构来代替内部数据结构,主要有:
当然了,因为这些结构是和内存直接打交道的,就有节省内存的优点,而又因为对内存的操作比较复杂,所以也有操作复杂,占用的CPU时间更多的缺点。 这个要掌握一个平衡,才能使redis的总体效率更好。目前,redis使用两种内存映射数据结构。 1. 整数集合 整数集合用于有序、无重复的保存多个整数值,它会根据元素的值,自动选择该用什么长度的整数类型来保存元素。比如,在一个int set中,最大的元素可以用int16_t保存,那么这个int set的所有元素都是int16_t,当插入一个元素是int32_t的时候,int set会先将所有元素升级为int32_t,再插入这个元素。总的来说,整数集合会自动升级。 看名字我们就知道它的用途:
那么我们看一下 intset 的定义: 1 typedef struct intset { 2 3 // 保存元素所使用的类型的长度 4 uint32_t encoding; 5 6 // 元素个数 7 uint32_t length; 8 9 // 保存元素的数组10 int8_t contents[]; 11 12 } intset; 其中 encoding 保存的是 intset 中元素的编码类型,比如是 int16_t还是 int32_t等等。具体的定义在 intset.c 中: 1 #define INTSET_ENC_INT16 (sizeof(int16_t))2 #define INTSET_ENC_INT32 (sizeof(int32_t))3 #define INTSET_ENC_INT64 (sizeof(int64_t)) length 肯定就是元素的个数喽,然后是具体的元素,我们发现是 int8_t 类型的,实现上它只是一个象征意义上的类型,到实际分配时候,会根据具体元素的类型选择合适的类型。而且 contents 有两个特点:
所以,添加元素到intset有下面几个步骤:
简单总结一下整数集合的特点:
2. 压缩列表 本质来说,压缩列表就是由一系列特殊编码的内存块构成的列表,一个压缩列表可以包含多个节点,每个节点可以保存一个长度受限的字符数组(不以为\0结尾的char数组)或者整数。说白了,它是以内存为中心的数据结构,一般列表是以元素类型的字节总数为大小,而压缩列表是以它最小内存块进行扩展组成的列表。下面我来说一下。 压缩列表分为3个部分:
其中压缩列表的节点值得说一下,它可以存储两类数据: 那么,怎样实现呢?很简单,通过 encoding + length 就可以搞定。encoding 占2位,00,01,10,11表示不明的类型,只有11代表的是节点中存放的是整型,其他3个代表节点中存放的都是字符串。而根据这2位的不同,又对应着不同的长度。 所以,由 encoding 可以知道元素的类型和这个元素的范围(比如 encoding 为01,包括 encoding 在内的2byte 代表长度,所以最长是214 - 1;如果 encoding 为00,包括 encoding 在内的1byte 代表元素的长度,所以最大值为26 -1 ) 然后添加元素大概是下面酱紫滴(对于列表来说,添加元素默认是加在列表尾巴的):
上面吐槽了压缩列表没有next指针,现在发现有了= =,但是不是指针,因为压缩列表会进行内存充分配,所以指针代表的内存地址需要一直维护,而当使用偏移量的话,就不需要更改一次维护一次。向后遍历是通过头指针+节点的大小(pre_entry_length+encoding+length的总大小)就可以跳到下一个节点了 不过,说实话,压缩列表这个设计的好处我还没有看到,可能还需要和后面的东西结合吧。 重读之后看到了,(^__^) 嘻嘻……
三前言这一章主要是讲redis内部的数据结构是如何实现的,可以说是redis的根基,前面2章介绍了redis的内部数据结构: redis的内存映射数据结构: 而这一章,就是具体将这些数据结构是如何在redis中工作的。 1. 总观redis内部实现一张图说明问题的本质:
之后,我们再根据这张图来说明redis中的数据架构为什么是酱紫滴。前面我们已经说过,redis中有5种数据结构,而它们的底层实现都不是唯一的,所以怎样选择对应的底层数据支撑呢?这就需要“多态”的思想,但是因为redis是C开发的。所以通过结构体来模仿对象的“多态”(当然,本质来说这是为了让自己能更好的理解)。 为了完成这个任务,redis是这样设计的:
下面看下redisObject的定义: /* * Redis 对象 */typedef struct redisObject { // 类型 unsigned type:4; // 对齐位 unsigned notused:2; // 编码方式 unsigned encoding:4; // LRU 时间(相对于 server.lruclock) unsigned lru:22; // 引用计数 int refcount; // 指向对象的值 void *ptr; } robj; 其中type、encoding、ptr是最重要的3个属性:
举个例子就是:
所以,当执行一个操作时,redis是这么干的:
然后reids还搞了一个内存共享,这个挺赞的:
如图所示:
三个列表的值分别为:
最后一个:redis对对象的管理是通过最原始的引用计数方法。 2. 字符串字符串是redis使用最多的数据结构,除了本身作为SET/GET的操作对象外,数据库中的所有key,以及执行命令时提供的参数,都是用字符串作为载体的。 在上面的图中,我们可以看见,字符串的底层可以有两种实现:
说白了就是除了long是通过第一种存储以外,其他类型都是通过第二种存储滴。 然后新创建的字符串,都会默认使用第二种编码,在将字符串作为键或者值保存进数据库时,程序会尝试转为第一种(为了节省空间) 3. 哈希表哈希表,嗯,它的底层实现也有两种:
当创建新的哈希表时,默认是使用压缩列表作为底层数据结构的,因为省内存呀。只有当触发了阈值才会转为字典:
4. 列表列表嘛,其实就是队列。它的底层实现也有2种:
当创建新的列表时,默认是使用压缩列表作为底层数据结构的,还是因为省内存- -。同样有一个触发阈值:
阻塞命令 对于列表,基本的操作就不介绍了,因为列表本身的操作和底层实现基本一致,所以我们可以简单的认为它具有双端队列的操作即可。重点讨论一下列表的阻塞命令比较好玩。 当我们执行BLPOP/BRPOP/BRPOPLPUSH的时候,都可能造成客户端的阻塞,它们被称为列表的阻塞原语,当然阻塞原语并不是一定会造成客户端阻塞:
上面两条的意思很简单,因为POP命令是删除一个节点,那么当没有节点的时候,客户端会阻塞直到一个元素添加进来,然后再执行POP命令,那么,对客户端的阻塞过程是这样的:
响应的,解铃须有系铃人:
上面的过程说的很简单,但是在redis内部要执行的操作可以很多的,我们用一段伪代码来演示一下被动脱离的过程: def handleClientsBlockedOnLists():# 执行直到 ready_keys 为空while server.ready_keys != NULL: # 弹出链表中的第一个 readyList rl = server.ready_keys.pop_first_node() # 遍历所有因为这个键而被阻塞的客户端 for client in all_client_blocking_by_key(rl.key, rl.db): # 只要还有客户端被这个键阻塞,就一直从键中弹出元素 # 如果被阻塞客户端执行的是 BLPOP ,那么对键执行 LPOP # 如果执行的是 BRPOP ,那么对键执行 RPOP element = rl.key.pop_element() if element == NULL: # 键为空,跳出 for 循环 # 余下的未解除阻塞的客户端只能等待下次新元素的进入了 break else: # 清除客户端的阻塞信息 server.blocking_keys.remove_blocking_info(client) # 将元素返回给客户端,脱离阻塞状态 client.reply_list_item(element) 至于主动脱离,更简单了,通过redis的cron job来检查时间,对于过期的blocking客户端,直接释放即可。伪代码如下: def server_cron_job(): # cron_job其他操作 ... # 遍历所有已连接客户端 for client in server.all_connected_client: # 如果客户端状态为“正在阻塞”,并且最大阻塞时限已到达 if client.state == BLOCKING and client.max_blocking_timestamp < current_timestamp(): # 那么给客户端发送空回复, 脱离阻塞状态 client.send_empty_reply() # 并清除客户端在服务器上的阻塞信息 server.blocking_keys.remove_blocking_info(client) # cron_job其他操作 ... 5. 集合这个就是set,底层实现有2种:
对于集合来说,和前面的2种不同点在于,集合的编码是决定于第一个添加进集合的元素:
同样,切换也需要达到一个阈值:
然后对于集合,有3个操作的算法很好玩,但是因为没用到过,就暂时列一下: 6. 有序集终于看到最后一个数据结构了,虽然只有5个- -。。。。首先从命令上就可以区分这几种了:
继续说有序集,这个东西我还真的没用过。。。其他最起码都了解过,这个算是第一次接触。现在看来,它也算一个sort过的map,sort的依据就是score,对score排序后得到的集合。 首先还是底层实现,有2种:
这个竟然用到了跳跃表,不用这个的话,跳跃表好像都快被我忘了呢。。对于编码的选择,和集合类似,也是决定于第一个添加进有序集的元素:
对于编码的转换阈值是这样的:
那我们知道,如果有序集是用ziplist实现的,而ziplist终对于member和score是按次序存储的,如member1,score1,member2,score2...这样的。那么,检索时候就蛋疼了,肯定是O(N)复杂度,既然这样,效率一下子就没有了。庆幸的是,转换成跳跃表之后,redis搞的很高明:
通过使用字典结构,并将 member 作为键,score 作为值,有序集可以在 O(1) 复杂度内:
通过使用跳跃表,可以让有序集支持以下两种操作:
通过同时使用字典和跳跃表,有序集可以高效地实现按成员查找和按顺序查找两种操作。所以,对于有序集来说,redis的思路确实是很流弊的。 7. 总结上面几个小节讲述了redis的数据结构的底层实现,但是没有涉及到具体的命令,如果调研后发现redis的某种数据结构满足需求,就可以对症下药,去查看redis对应的API即可。 四前言这一章主要讲解redis内部的一些功能,主要分为以下4个: 那么,我们就来逐个击破! 1. 事务事务对于刚接触计算机的人来说可能会比较抽象。因为事务是对计算机某些操作的称谓。通俗来说,事务就是一个命令、一组命令执行的最小单元。事务一般具有ACID属性(redis只支持两种,下文详细说明):
那么,redis是通过MULTI/DISCARD/EXEC/WATCH这4个命令来实现事务功能。对事务,我们必须知道事务安全性是一个非常重要的。 事务提供了一种“将多个命令打包,然后一次性、按顺序执行”的机制,并且在事务执行期间不会中断——意思就是在事务完成之前,客户端的其他命令都是阻塞状态。 以下是一个事务的例子,它先以 MULTI 开始一个事务,然后将多个命令入队到事务中,最后 由EXEC 命令触发事务,一并执行事务中的所有命令: redis> MULTI OK redis> SET book-name "Mastering C++ in 21 days"?81QUEUED redis> GET book-nameQUEUED redis> SADD tag "C++" "Programming" "Mastering Series"QUEUED redis> SMEMBERS tagQUEUED redis> EXEC1) OK2) "Mastering C++ in 21 days"3) (integer) 34) 1) "Mastering Series" 2) "C++" 3) "Programming" 一个事务主要经历3个阶段:
这几个过程都比较简单,开始事务就是切换到事务模式;命令入队就是把事务中的每条命令记录下来,包括是第几条命令,命令参数什么的(当然,事务中是不能再嵌套事务的,所以再有事务关键字(MULTI/DISCARD/WATCH)会立即执行的);执行事务就是一下子把刚才那个事务的命令执行完。
对于上面的WATCH来说,我们可以看成一个锁。这个锁在执行期间是不可以修改(类比为打开锁)的,这样才能保证这次事务是隔离的,安全的。那么,WATCH是如何触发的呢?
事务的ACID性质 前面说到,事务一般具有ACID属性,但是redis只保证两种机制:一致性和隔离性。对于原子性和持久性并没有支持,下面说明redis为什么这样做。
2. 订阅与发布这个东西没有仔细看,但是大概知道是啥功能的。我想了一下,可以使用这个功能来完成跨平台之间消息的推送。比如我开发了一个app,分别有web版本、ios版本、Android版本、Symbian版本。那么,我可以结合模式+频道,将消息推送到所有安装此应用的平台上。 3. Lua脚本这是redis2.6版本最大的亮点。但是我们好像木有用过- -所以,以后有需求的时候再好好研究一下吧。 4. 慢查询日志慢查询日志是redis系统提供的一个查看系统性能的功能。它的每一条记录的是一条命令的执行时间。所以,你可以在redis.conf中设置当超过slowlog_log_slower_than的时候,将这个命令记录下来;因为慢查询日志是一个FIFO队列(用链表实现的),所以还有一个slowlog_max_than来限制队列长度,如果溢出,就从队头删除最旧的,将最新的添加到队尾。 五前言这一章是讲redis内部运作机制的,所以算是redis的核心。在这一章中,将会学习到redis是如何设计成为一个非常好用的nosql数据库的。下面我们将要讨论这些话题:
带着这几个问题,我们就来学习一下redis的内部运作机制,当然,我们重点是学习它为什么要这样设计,这样设计为什么是最优的?有没有可以改进的地方呢?对细节不必太追究,先从整体上理解redis的框架是如何搭配的,然后对哪个模块感兴趣再去看看源码,好像2.6版本的代码量在5W行左右吧。 1. 数据库嗯,好像一直用的都是默认的数据库。废话不说,直接上一个数据库结构: typedef struct redisDb { //数据库编号 int id; //保存数据库所有键值对数据,也成为键空间(key space) dict *dict; //保存着键的过期信息 dict *expires; //实现列表阻塞原语,如BLPOP dict *blocking_keys; dict *ready_keys; //用于实现WATCH命令 dict *watched_keys } 主要来介绍3个属性:
这其中比较好的一个是redis对于过期键的处理,我当时看到这里想,可以弄一个定时器,定期来检查expire字典中的key是否到了过期时间,但是这个定时器的时间间隔不好控制,长了的话已经过期的键还可以访问;短了的话,又注定会影像系统的性能。
lazy机制:
所以,redis综合考虑后采用了懒惰删除和定期删除,这两个策略相互配合,可以很好的完成CPU和内存的平衡。 2. RDB因为当前项目用到了这个,必须要好好看看啊。战略上藐视一下,就是redis数据库从内存持久化到文件的意思。redis一共有两种持久化操作: 逐个来说,先搞定RDB。 对于RDB机制来说,在保存RDB文件期间,主进程会被阻塞,直到保存成功为止。但是这也分两种实现:
整个流程可以用伪代码表示: def SAVE(): rdbSave()def BGSAVE(): pid = fork() if pid == 0: # 子进程保存 RDB rdbSave() elif pid > 0: # 父进程继续处理请求,并等待子进程的完成信号 handle_request() else: # pid == -1 # 处理 fork 错误 handle_fork_error() 当然,写入之后就是load了。当redis服务重启,就会将存在的dump.rdb文件重新载入到内存中,用于数据恢复,那么redis是怎么做的呢? 额,这一节重点是RDB文件的结构,如果有兴趣,可以自己去看下dump.rdb文件,然后对照一下很容易就明白了。 3. AOFAOF是append only file的缩写,意思是追加到唯一的文件,从上面对RDB的介绍我们知道,RDB的写入是触发式的,等待多少秒或者多少次写入才会持久化到文件中,但是AOF是实时的,它会记录你的每一个命令。 同步到AOF文件的整个过程可以分为三个阶段:
对于第三点我们需要说明一下,在前面我们说到,RDB是触发式的,AOF是实时的。这里怎么又说也是满足条件了呢?原来redis对于这个条件,有以下的方式:
补充:对于第二种的流程可能比较麻烦,用一个图来说明:
如果仔细看上面的条件,会发现一会SAVE是子线程执行的,一会是主进程执行的,那么怎样从根本上区分呢?
对于上面三种方式来说,最好的应该是第二种,因为阻塞操作会让 Redis 主进程无法持续处理请求,所以一般说来,阻塞操作执行得越少、完成得越快,Redis 的性能就越好。
AOF文件的还原 对于AOF文件的还原就特别简单了,因为AOF是按照AOF协议保存的redis操作命令,所以redis会伪造一个客户端,把AOF保存的命令重新执行一遍,执行之后就会得到一个完成的数据库,伪代码如下: def READ_AND_LOAD_AOF(): # 打开并读取 AOF 文件 file = open(aof_file_name) while file.is_not_reach_eof(): # 读入一条协议文本格式的 Redis 命令 cmd_in_text = file.read_next_command_in_protocol_format() # 根据文本命令,查找命令函数,并创建参数和参数个数等对象 cmd, argv, argc = text_to_command(cmd_in_text) # 执行命令 execRedisCommand(cmd, argv, argc) # 关闭文件 file.close() AOF重写 上面提到,AOF可以对redis的每个操作都记录,但这带来一个问题,当redis的操作越来越多之后,AOF文件会变得很大。而且,里面很大一部分都是无用的操作,你如我对一个整型+1,然后-1,然后再加1,然后再-1(比如这是一个互斥锁的开关),那么,过一段时间后,可能+1、-1操作就执行了几万次,这时候,如果能对AOF重写,把无效的命令清除,AOF会明显瘦身,这样既可以减少AOF的体积,在恢复的时候,也能用最短的指令和最少的时间来恢复整个数据库,迫于这个构想,redis提供了对AOF的重写。 所谓的重写呢,其实说的不够明确。因为redis所针对的重写实际上指数据库中键的当前值。AOF 重写是一个有歧义的名字,实际的重写工作是针对数据库的当前值来进行的,程序既不读写、也不使用原有的 AOF 文件。比如现在有一个列表,push了1、2、3、4,然后删除4、删除1、加入1,这样列表最后的元素是1、2、3,如果不进行缩减,AOF会记录4次redis操作,但是AOF重写它看的是列表最后的值:1、2、3,于是它会用一条rpush 1 2 3来完成,这样由4条变为1条命令,恢复到最近的状态的代价就变为最小。 整个重写过程的伪代码如下: def AOF_REWRITE(tmp_tile_name): f = create(tmp_tile_name) # 遍历所有数据库 for db in redisServer.db: # 如果数据库为空,那么跳过这个数据库 if db.is_empty(): continue # 写入 SELECT 命令,用于切换数据库 f.write_command("SELECT " + db.number) # 遍历所有键 for key in db: # 如果键带有过期时间,并且已经过期,那么跳过这个键 if key.have_expire_time() and key.is_expired(): continue if key.type == String: # 用 SET key value 命令来保存字符串键 value = get_value_from_string(key) f.write_command("SET " + key + value) elif key.type == List: # 用 RPUSH key item1 item2 ... itemN 命令来保存列表键 item1, item2, ..., itemN = get_item_from_list(key) f.write_command("RPUSH " + key + item1 + item2 + ... + itemN) elif key.type == Set: # 用 SADD key member1 member2 ... memberN 命令来保存集合键 member1, member2, ..., memberN = get_member_from_set(key) f.write_command("SADD " + key + member1 + member2 + ... + memberN) elif key.type == Hash: # 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键 field1, value1, field2, value2, ..., fieldN, valueN = get_field_and_value_from_hash(key) f.write_command("HMSET " + key + field1 + value1 + field2 + value2 + ... + fieldN + valueN) elif key.type == SortedSet: # 用 ZADD key score1 member1 score2 member2 ... scoreN memberN # 命令来保存有序集键 score1, member1, score2, member2, ..., scoreN, memberN = get_score_and_member_from_sorted_set(key) f.write_command("ZADD " + key + score1 + member1 + score2 + member2 + ... + scoreN + memberN) else: raise_type_error() # 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间 if key.have_expire_time(): f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp()) # 关闭文件 f.close() AOF重写的一个问题:如何实现重写? 是使用后台线程还是使用子进程(redis是单进程的),这个问题值得讨论下。额,对进程线程只是概念级的,等看完之后得拿redis的进程、线程机制开刀好好学一下。 redis肯定是以效率为先,所以不希望AOF重写造成客户端无法请求,所以redis采用了AOF重写子进程执行,这样的好处有:
当然,有有点肯定有缺点:
为了解决这个问题,redis增加了一个AOF重写缓存(在内存中),这个缓存在fort出子进程之后开始启用,redis主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到AOF文件之外,还会同时追加到这个缓存中。这样,当子进程完成AOF重写之后,它会给主进程发送一个信号,主进程接收信号后,会将AOF重写缓存中的内容全部写入新AOF文件中,然后对新AOF改名,覆盖老的AOF文件。 在整个AOF重写过程中,只有最后的写入缓存和改名操作会造成主进程的阻塞(要是不阻塞,客户端请求到达又会造成数据不一致),所以,整个过程将AOF重写对性能的消耗降到了最低。 AOF触发条件 最后说一下AOF是如何触发的,当然,如果手动触发,是通过BGREWRITEAOF执行的。如果要用redis的自动触发,就要涉及下面3个变量(AOF的功能要开启哦 appendonlyfile yes):
每当serverCron函数(redis的crontab)执行时,会检查以下条件是否全部满足,如果是的话,就会触发自动的AOF重写:
默认情况下,增长百分比为100%。也就是说,如果前面三个条件已经满足,并且当前AOF文件大小比最后一次AOF重写的大小大一倍就会触发自动AOF重写。 |
自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习
京ICP备14009008号-1@版权所有www.zixuephp.com
网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com