memcached和redis,做为近些年最经常使用的缓存服务器,相信你们对它们再熟悉不过了。前两年还在学校时,我曾经读过它们的主要源码,现在写篇笔记从我的角度简单对比一下它们的实现方式,权当作复习, 有理解错误之处,欢迎指正。react
读一个软件的源码,首先要弄懂软件是用做干什么的,那memcached和redis是干啥的?众所周知,数据通常会放在数据库中,可是查询数据会相对比较慢,特别是用户不少时,频繁的查询,须要耗费大量的时间。怎么办呢?数据放在哪里查询快?那确定是内存中。memcached和redis就是将数据存储在内存中,按照key-value的方式查询,能够大幅度提升效率。因此通常它们都用作缓存服务器,缓存经常使用的数据,须要查询的时候,直接从它们那儿获取,减小查询数据库的次数,提升查询效率。nginx
memcached和redis怎么提供服务呢?它们是独立的进程, 须要的话,还可让他们变成daemon进程,因此咱们的用户进程要使用memcached和redis的服务的话,就须要进程间通讯了。 考虑到用户进程和memcached和redis不必定在同一台机器上,因此还须要支持网络间通讯。 所以,memcached和redis本身自己就是网络服务器,用户进程经过与他们经过网络来传输数据,显然最简单和最经常使用的就是使用tcp链接了。另外,memcached和redis都支持udp协议。 并且当用户进程和memcached和redis在同一机器时,还可使用unix域套接字通讯。c++
下面开始讲他们具体是怎么实现的了。首先来看一下它们的事件模型。web
自从epoll出来之后,几乎全部的网络服务器全都抛弃select和poll,换成了epoll。redis也同样,只很少它还提供对select和poll的支持,能够本身配置使用哪个,可是通常都是用epoll。另外针对BSD,还支持使用kqueue。而memcached是基于libevent的,不过libevent底层也是使用epoll的,因此能够认为它们(memcached/redis)都是使用epoll。 epoll的特性这里就不介绍了,网上介绍文章不少。redis
它们都使用epoll来作事件循环,不过redis是单线程的服务器(redis也是多线程的,只不过除了主线程之外,其余线程没有event loop,只是会进行一些后台存储工做),而memcached是多线程的。 redis的事件模型很简单,只有一个event loop,是简单的reactor实现。 不过redis事件模型中有一个亮点,咱们知道epoll是针对fd的,它返回的就绪事件也是只有fd,redis里面的fd就是服务器与客户端链接的socket的fd, 可是处理的时候,须要根据这个fd找到具体的客户端的信息,怎么找呢?一般的处理方式就是用红黑树将fd与客户端信息保存起来,经过fd查找,效率是lgn。不过redis比较特殊,redis的客户端的数量上限能够设置,便可以知道同一时刻,redis所打开的fd的上限,而咱们知道,进程的fd在同一时刻是不会重复的(fd只有关闭后才能复用),因此redis使用一个数组,将fd做为数组的下标,数组的元素就是客户端的信息,这样,直接经过fd就能定位客户端信息,查找效率是O(1),还省去了复杂的红黑树的实现(我曾经用c写一个网络服务器,就由于要保持fd和connect对应关系,不想本身写红黑树,而后用了STL里面的set,致使项目变成了c++的,最后项目使用g++编译,这事我不说谁知道?)。显然这种方式只能针对connection数量上限已肯定,而且不是太大的网络服务器,像nginx这种http服务器就不适用,nginx就是本身写了红黑树。数据库
而memcached是多线程的,使用master-worker的方式,主线程监听端口,创建链接,而后顺序分配给各个工做线程。每个从线程都有一个event loop,它们服务不一样的客户端。 master线程和worker线程之间使用管道通讯,每个工做线程都会建立一个管道,而后保存写端和读端,而且将读端加入event loop,监听可读事件。同时,每一个从线程都有一个就绪链接队列,主线程链接链接后,将链接的item放入这个队列,而后往该线程的管道的写端写入一个connect命令,这样event loop中加入的管道读端就会就绪,从线程读取命令,解析命令发现是有链接,而后就会去本身的就绪队列中获取链接,并进行处理。多线程的优点就是能够充分发挥多核的优点,不过编写程序麻烦一点,memcached里面就有各类锁和条件变量来进行线程同步。数组
memcached和redis的核心任务都是在内存中操做数据,内存管理天然是核心的内容。缓存
首先看看他们的内存分配方式。memcached是有本身得内存池的,即预先分配一大块内存,而后接下来分配内存就从内存池中分配,这样能够减小内存分配的次数,提升效率,这也是大部分网络服务器的实现方式, 只不过各个内存池的管理方式根据具体状况而不一样。而redis没有本身得内存池,而是直接使用时分配,即何时须要何时分配,内存管理的事交给内核,本身只负责取和释放 (redis既是单线程,又没有本身的内存池,是否是感受实现的太简单了?那是由于它的重点都放在数据库模块了)。不过redis支持使用tcmalloc来替换glibc的malloc,前者是google的产品,比glibc的malloc快。服务器
因为redis没有本身的内存池,因此内存申请和释放的管理就简单不少,直接malloc和free便可,十分方便。而memcached是支持内存池的,因此内存申请是从内存池中获取,而free也是还给内存池,因此须要不少额外的管理操做,实现起来麻烦不少,具体的会在后面memcached的slab机制讲解中分析。网络
接下来看看他们的最核心内容,各自数据库的实现。
memcached只支持key-value,即只能一个key对于一个value。 它的数据在内存中也是这样以key-value对的方式存储,它使用slab机制。
首先看memcached是如何存储数据的,即存储key-value对。以下图,每个key-value对都存储在一个item结构中,包含了相关的属性和key和value的值。
item是保存key-value对的,当item多的时候,怎么查找特定的item是个问题。因此memcached维护了一个hash表,它用于快速查找item。hash表适用开链法(与redis同样)解决键的冲突, 每个hash表的桶里面存储了一个链表,链表节点就是item的指针,如上图中的h_next就是指桶里面的链表的下一个节点。 hash表支持扩容( item的数量是桶的数量的1.5以上时扩容 ),有一个primary_hashtable,还有一个old_hashtable,其中正常适用primary_hashtable,可是扩容的时候,将old_hashtable = primary_hashtable,而后primary_hashtable设置为新申请的hash表(桶的数量乘以2),而后依次将old_hashtable 里面的数据往新的hash表里面移动,并用一个变量expand_bucket记录以及移动了多少个桶,移动完成后,再free原来的old_hashtable 便可( redis也是有两个hash表,也是移动,不过不是后台线程完成,而是每次移动一个桶 )。扩容的操做,专门有一个后台扩容的线程来完成,须要扩容的时候,使用条件变量通知它,完成扩容后,它又考试阻塞等待扩容的条件变量。这样在扩容的时候,查找一个item可能会在primary_hashtable和old_hashtable的任意一个中,须要根据比较它的桶的位置和expand_bucket的大小来比较肯定它在哪一个表里。
item是从哪里分配的呢?从slab中。以下图,memcached有不少slabclass,它们管理slab,每个slab实际上是trunk的集合,真正的item是在trunk中分配的,一个trunk分配一个item。一个slab中的trunk的大小同样,不一样的slab,trunk的大小按比例递增,须要新申请一个item的时候,根据它的大小来选择trunk,规则是比它大的最小的那个trunk。这样,不一样大小的item就分配在不一样的slab中,归不一样的slabclass管理。 这样的缺点是会有部份内存浪费,由于一个trunk可能比item大,如图2,分配100B的item的时候,选择112的trunk,可是会有12B的浪费,这部份内存资源没有使用。
如上图,整个构造就是这样,slabclass管理slab,一个slabclass有一个slab_list,能够管理多个slab,同一个slabclass中的slab的trunk大小都同样。slabclass有一个指针slot,保存了未分配的item已经被free掉的item(不是真的free内存,只是不用了而已),有item不用的时候,就放入slot的头部,这样每次须要在当前slab中分配item的时候,直接取slot取便可,不用管item是未分配过的仍是被释放掉的。
而后,每个slabclass对应一个链表,有head数组和tail数组,它们分别保存了链表的头节点和尾节点。链表中的节点就是改slabclass所分配的item,新分配的放在头部,链表越日后的item,表示它已经好久没有被使用了。当slabclass的内存不足,须要删除一些过时item的时候,就能够从链表的尾部开始删除,没错,这个链表就是为了实现LRU。光靠它还不行,由于链表的查询是O(n)的,因此定位item的时候,使用hash表,这已经有了,全部分配的item已经在hash表中了,因此,hash用于查找item,而后链表有用存储item的最近使用顺序,这也是lru的标准实现方法。
每次须要新分配item的时候,找到slabclass对于的链表,从尾部往前找,看item是否已通过期,过时的话,直接就用这个过时的item当作新的item。没有过时的,则须要从slab中分配trunk,若是slab用完了,则须要往slabclass中添加slab了。
memcached支持设置过时时间,即expire time,可是内部并不按期检查数据是否过时,而是客户进程使用该数据的时候,memcached会检查expire time,若是过时,直接返回错误。这样的优势是,不须要额外的cpu来进行expire time的检查,缺点是有可能过时数据好久不被使用,则一直没有被释放,占用内存。
memcached是多线程的,并且只维护了一个数据库,因此可能有多个客户进程操做同一个数据,这就有可能产生问题。好比,A已经把数据更改了,而后B也更改了改数据,那么A的操做就被覆盖了,而可能A不知道,A任务数据如今的状态时他改完后的那个值,这样就可能产生问题。为了解决这个问题,memcached使用了CAS协议,简单说就是item保存一个64位的unsigned int值,标记数据的版本,每更新一次(数据值有修改),版本号增长, 而后每次对数据进行更改操做,须要比对客户进程传来的版本号和服务器这边item的版本号是否一致,一致则可进行更改操做,不然提示脏数据。
以上就是memcached如何实现一个key-value的数据库的介绍。
首先redis数据库的功能强大一些,由于不像memcached只支持保存字符串,redis支持string, list, set,sorted set,hash table 5种数据结构。 例如存储一我的的信息就可使用hash table,用人的名字作key,而后name super, age 24, 经过key 和 name,就能够取到名字super,或者经过key和age,就能够取到年龄24。这样,当只须要取得age的时候,不须要把人的整个信息取回来,而后从里面找age,直接获取age便可,高效方便。
为了实现这些数据结构,redis定义了抽象的对象redis object,以下图。每个对象有类型,一共5种:字符串,链表,集合,有序集合,哈希表。 同时,为了提升效率,redis为每种类型准备了多种实现方式,根据特定的场景来选择合适的实现方式, encoding就是表示对象的实现方式的。而后还有记录了对象的lru,即上次被访问的时间,同时在redis 服务器中会记录一个当前的时间(近似值,由于这个时间只是每隔必定时间,服务器进行自动维护的时候才更新),它们两个只差就能够计算出对象多久没有被访问了。 而后redis object中还有引用计数,这是为了共享对象,而后肯定对象的删除时间用的。最后使用一个void*指针来指向对象的真正内容。正式因为使用了抽象redis object,使得数据库操做数据时方便不少,所有统一使用redis object对象便可,须要区分对象类型的时候,再根据type来判断。并且正式因为采用了这种面向对象的方法,让redis的代码看起来很像c++代码,其实全是用c写的。
说到底redis仍是一个key-value的数据库,无论它支持多少种数据结构,最终存储的仍是以key-value的方式,只不过value能够是链表,set,sorted set,hash table等。和memcached同样,全部的key都是string,而set,sorted set,hash table等具体存储的时候也用到了string。 而c没有现成的string,因此redis的首要任务就是实现一个string,取名叫sds(simple dynamic string),以下的代码, 很是简单的一个结构体,len存储改string的内存总长度,free表示还有多少字节没有使用,而buf存储具体的数据,显然len-free就是目前字符串的长度。
struct sdshdr {
int len;
int free;
char buf[];
};Â
字符串解决了,全部的key都存成sds就好了,那么key和value怎么关联呢?key-value的格式在脚本语言中很好处理,直接使用字典便可,C没有字典,怎么办呢?本身写一个呗(redis十分热衷于造轮子)。看下面的代码,privdata存额外信息,用的不多,至少咱们发现。 dictht是具体的哈希表,一个dict对应两张哈希表,这是为了扩容(包括rehashidx也是为了扩容)。dictType存储了哈希表的属性。redis还为dict实现了迭代器(因此说看起来像c++代码)。
哈希表的具体实现是和mc相似的作法,也是使用开链法来解决冲突,不过里面用到了一些小技巧。好比使用dictType存储函数指针,能够动态配置桶里面元素的操做方法。又好比dictht中保存的sizemask取size(桶的数量)-1,用它与key作&操做来代替取余运算,加快速度等等。总的来看,dict里面有两个哈希表,每一个哈希表的桶里面存储dictEntry链表,dictEntry存储具体的key和value。
前面说过,一个dict对于两个dictht,是为了扩容(其实还有缩容)。正常的时候,dict只使用dictht[0],当dict[0]中已有entry的数量与桶的数量达到必定的比例后,就会触发扩容和缩容操做,咱们统称为rehash,这时,为dictht[1]申请rehash后的大小的内存,而后把dictht[0]里的数据往dictht[1]里面移动,并用rehashidx记录当前已经移动万的桶的数量,当全部桶都移完后,rehash完成,这时将dictht[1]变成dictht[0], 将原来的dictht[0]变成dictht[1],并变为null便可。不一样于memcached,这里不用开一个后台线程来作,而是就在event loop中完成,而且rehash不是一次性完成,而是分红屡次,每次用户操做dict以前,redis移动一个桶的数据,直到rehash完成。这样就把移动分红多个小移动完成,把rehash的时间开销均分到用户每一个操做上,这样避免了用户一个请求致使rehash的时候,须要等待很长时间,直到rehash完成才有返回的状况。 不过在rehash期间,每一个操做都变慢了点,并且用户还不知道redis在他的请求中间添加了移动数据的操做,感受redis太贱了 :-D
typedef struct dict {
dictType \*type; // 哈希表的相关属性
void \*privdata; // 额外信息
dictht ht[2]; // 两张哈希表,分主和副,用于扩容
int rehashidx; /* rehashing not in progress if rehashidx == -1 \*/ // 记录当前数据迁移的位置,在扩容的时候用的
int iterators; /* number of iterators currently running \*/ // 目前存在的迭代器的数量
} dict;
typedef struct dictht {
dictEntry \**table; // dictEntry是item,多个item组成hash桶里面的链表,table则是多个链表头指针组成的数组的指针
unsigned long size; // 这个就是桶的数量
// sizemask取size - 1, 而后一个数据来的时候,经过计算出的hashkey, 让hashkey & sizemask来肯定它要放的桶的位置
// 当size取2^n的时候,sizemask就是1...111,这样就和hashkey % size有同样的效果,可是使用&会快不少。这就是缘由
unsigned long sizemask;
unsigned long used; // 已经数值的dictEntry数量
} dictht;
typedef struct dictType {
unsigned int (\*hashFunction)(const void \*key); // hash的方法
void \*(\*keyDup)(void \*privdata, const void \*key); // key的复制方法
void \*(\*valDup)(void \*privdata, const void \*obj); // value的复制方法
int (\*keyCompare)(void \*privdata, const void \*key1, const void \*key2); // key之间的比较
void (\*keyDestructor)(void \*privdata, void \*key); // key的析构
void (\*valDestructor)(void \*privdata, void \*obj); // value的析构
} dictType;
typedef struct dictEntry {
void \*key;
union {
void \*val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry \*next;
} dictEntry;
有了dict,数据库就好实现了。全部数据读存储在dict中,key存储成dictEntry中的key(string),用void* 指向一个redis object,它能够是5种类型中的任何一种。以下图,结构构造是这样,不过这个图已通过时了,有一些与redis3.0不符合的地方。
5种type的对象,每个都至少有两种底层实现方式。 string有3种:REDIS_ENCODING_RAW, REDIS_ENCIDING_INT, REDIS_ENCODING_EMBSTR, list有:普通双向链表和压缩链表,压缩链表简单的说,就是讲数组改形成链表,连续的空间,而后经过存储字符串的大小信息来模拟链表,相对普通链表来讲能够节省空间,不过有反作用,因为是连续的空间,因此改变内存大小的时候,须要从新分配,而且因为保存了字符串的字节大小,全部有可能引发连续更新(具体实现请详细看代码)。set有dict和intset(全是整数的时候使用它来存储), sorted set有:skiplist和ziplist, hashtable实现有压缩列表和dict和ziplist。skiplist就是跳表,它有接近于红黑树的效率,可是实现起来比红黑树简单不少,因此被采用(奇怪,这里又不造轮子了,难道由于这个轮子有点难?)。 hash table可使用dict实现,则改dict中,每一个dictentry中key保存了key(这是哈希表中的键值对的key),而value则保存了value,它们都是string。 而set中的dict,每一个dictentry中key保存了set中具体的一个元素的值,value则为null。图中的zset(有序集合)有误,zset使用skiplist和ziplist实现,首先skiplist很好理解,就把它当作红黑树的替代品就行,和红黑树同样,它也能够排序。怎么用ziplist存储zset呢?首先在zset中,每一个set中的元素都有一个分值score,用它来排序。因此在ziplist中,按照分值大小,先存元素,再存它的score,再存下一个元素,而后score。这样连续存储,因此插入或者删除的时候,都须要从新分配内存。因此当元素超过必定数量,或者某个元素的字符数超过必定数量,redis就会选择使用skiplist来实现zset(若是当前使用的是ziplist,会将这个ziplist中的数据取出,存入一个新的skiplist,而后删除改ziplist,这就是底层实现转换,其他类型的redis object也是能够转换的)。 另外,ziplist如何实现hashtable呢?其实也很简单,就是存储一个key,存储一个value,再存储一个key,再存储一个value。仍是顺序存储,与zset实现相似,因此当元素超过必定数量,或者某个元素的字符数超过必定数量时,就会转换成hashtable来实现。各类底层实现方式是能够转换的,redis能够根据状况选择最合适的实现方式,这也是这样使用相似面向对象的实现方式的好处。
须要指出的是,使用skiplist来实现zset的时候,其实还用了一个dict,这个dict存储同样的键值对。为何呢?由于skiplist的查找只是lgn的(可能变成n),而dict能够到O(1), 因此使用一个dict来加速查找,因为skiplist和dict能够指向同一个redis object,因此不会浪费太多内存。另外使用ziplist实现zset的时候,为何不用dict来加速查找呢?由于ziplist支持的元素个数不多(个数多时就转换成skiplist了),顺序遍历也很快,因此不用dict了。
这样看来,上面的dict,dictType,dictHt,dictEntry,redis object都是颇有考量的,它们配合实现了一个具备面向对象色彩的灵活、高效数据库。不得不说,redis数据库的设计仍是很厉害的。
与memcached不一样的是,redis的数据库不止一个,默认就有16个,编号0-15。客户能够选择使用哪个数据库,默认使用0号数据库。 不一样的数据库数据不共享,即在不一样的数据库中能够存在一样的key,可是在同一个数据库中,key必须是惟一的。
redis也支持expire time的设置,咱们看上面的redis object,里面没有保存expire的字段,那redis怎么记录数据的expire time呢? redis是为每一个数据库又增长了一个dict,这个dict叫expire dict,它里面的dict entry里面的key就是数对的key,而value全是数据为64位int的redis object,这个int就是expire time。 这样,判断一个key是否过时的时候,去expire dict里面找到它,取出expire time比对当前时间便可。为何这样作呢? 由于并非全部的key都会设置过时时间,因此,对于不设置expire time的key来讲,保存一个expire time会浪费空间,而是用expire dict来单独保存的话,能够根据须要灵活使用内存(检测到key过时时,会把它从expire dict中删除)。
redis的expire 机制是怎样的呢? 与memcahed相似,redis也是惰性删除,即要用到数据时,先检查key是否过时,过时则删除,而后返回错误。 单纯的靠惰性删除,上面说过可能会致使内存浪费,因此redis也有补充方案,redis里面有个定时执行的函数,叫servercron,它是维护服务器的函数,在它里面,会对过时数据进行删除,注意不是全删,而是在必定的时间内,对每一个数据库的expire dict里面的数据随机选取出来,若是过时,则删除,不然再选,直到规定的时间到。即随机选取过时的数据删除,这个操做的时间分两种,一种较长,一种较短,通常执行短期的删除,每隔必定的时间,执行一次长时间的删除。 这样能够有效的缓解光采用惰性删除而致使的内存浪费问题。
以上就是redis的数据的实现,与memcached不一样,redis还支持数据持久化,这个下面介绍。
redis和memcached的最大不一样,就是redis支持数据持久化,这也是不少人选择使用redis而不是memcached的最大缘由。 redis的持久化,分为两种策略,用户能够配置使用不一样的策略。
用户执行save或者bgsave的时候,就会触发RDB持久化操做。RDB持久化操做的核心思想就是把数据库原封不动的保存在文件里。
那如何存储呢?以下图, 首先存储一个REDIS字符串,起到验证的做用,表示是RDB文件,而后保存redis的版本信息,而后是具体的数据库,而后存储结束符EOF,最后用检验和。关键就是databases,看它的名字也知道,它存储了多个数据库,数据库按照编号顺序存储,0号数据库存储完了,才轮到1,而后是2, 一直到最后一个数据库。
每个数据库存储方式以下,首先一个1字节的常量SELECTDB,表示切换db了,而后下一个接上数据库的编号,它的长度是可变的,而后接下来就是具体的key-value对的数据了。
由上面的代码也能够看出,存储的时候,先检查expire time,若是已通过期,不存就好了,不然,则将expire time存下来,注意,及时是存储expire time,也是先存储它的类型为REDIS_RDB_OPCODE_EXPIRETIME_MS,而后再存储具体过时时间。接下来存储真正的key-value对,首先存储value的类型,而后存储key(它按照字符串存储),而后存储value,以下图。
在rdbsaveobject中,会根据val的不一样类型,按照不一样的方式存储,不过从根本上来看,最终都是转换成字符串存储,好比val是一个linklist,那么先存储整个list的字节数,而后遍历这个list,把数据取出来,依次按照string写入文件。对于hash table,也是先计算字节数,而后依次取出hash table中的dictEntry,按照string的方式存储它的key和value,而后存储下一个dictEntry。 总之,RDB的存储方式,对一个key-value对,会先存储expire time(若是有的话),而后是value的类型,而后存储key(字符串方式),而后根据value的类型和底层实现方式,将value转换成字符串存储。 这里面为了实现数据压缩,以及可以根据文件恢复数据,redis使用了不少编码的技巧,有些我也没太看懂,不过关键仍是要理解思想,不要在乎这些细节。
保存了RDB文件,当redis再启动的时候,就根据RDB文件来恢复数据库。因为以及在RDB文件中保存了数据库的号码,以及它包含的key-value对,以及每一个key-value对中value的具体类型,实现方式,和数据,redis只要顺序读取文件,而后恢复object便可。因为保存了expire time,发现当前的时间已经比expire time大了,即数据已经超时了,则不恢复这个key-value对便可。
保存RDB文件是一个很巨大的工程,因此redis还提供后台保存的机制。即执行bgsave的时候,redis fork出一个子进程,让子进程来执行保存的工做,而父进程继续提供redis正常的数据库服务。因为子进程复制了父进程的地址空间,即子进程拥有父进程fork时的数据库,子进程执行save的操做,把它从父进程那儿继承来的数据库写入一个temp文件便可。在子进程复制期间,redis会记录数据库的修改次数(dirty)。当子进程完成时,发送给父进程SIGUSR1信号,父进程捕捉到这个信号,就知道子进程完成了复制,而后父进程将子进程保存的temp文件更名为真正的rdb文件( 即真正保存成功了才改为目标文件,这才是保险的作法 )。而后记录下这一次save的结束时间。
这里有一个问题,在子进程保存期间,父进程的数据库已经被修改了,而父进程只是记录了修改的次数(dirty),被没有进行修正操做。彷佛使得RDB保存的不是实时的数据库,有点不过高大上的样子。 不事后面要介绍的AOF持久化,就解决了这个问题。
除了客户执行sava或者bgsave命令,还能够配置RDB保存条件。即在配置文件中配置,在t时间内,数据库被修改了dirty次,则进行后台保存。redis在serve cron的时候,会根据dirty数目和上次保存的时间,来判断是否符合条件,符合条件的话,就进行bg save,注意,任意时刻只能有一个子进程来进行后台保存,由于保存是个很费io的操做,多个进程大量io效率不行,并且很差管理。
首先想一个问题,保存数据库必定须要像RDB那样把数据库里面的全部数据保存下来么?有没有别的方法?
RDB保存的只是最终的数据库,它是一个结果。结果是怎么来的?是经过用户的各个命令创建起来的,因此能够不保存结果,而只保存创建这个结果的命令。redis的AOF就是这个思想,它不一样RDB保存db的数据,它保存的是一条一条创建数据库的命令。
咱们首先来看AOF文件的格式,它里面保存的是一条一条的命令,首先存储命令长度,而后存储命令,具体的分隔符什么的能够本身深刻研究,这都不是重点,反正知道AOF文件存储的是redis客户端执行的命令便可。
redis server中有一个sds aof_buf, 若是aof持久化打开的话,每一个修改数据库的命令都会存入这个aof_buf(保存的是aof文件中命令格式的字符串),而后event loop每循环一次,在server cron中调用flushaofbuf,把aof_buf中的命令写入aof文件(实际上是write,真正写入的是内核缓冲区),再清空aof_buf,进入下一次loop。这样全部的数据库的变化,均可以经过aof文件中的命令来还原,达到了保存数据库的效果。
须要注意的是,flushaofbuf中调用的write,它只是把数据写入了内核缓冲区,真正写入文件时内核本身决定的,可能须要延后一段时间。 不过redis支持配置,能够配置每次写入后sync,则在redis里面调用sync,将内核中的数据写入文件,这不过这要耗费一次系统调用,耗费时间而已。还能够配置策略为1秒钟sync一次,则redis会开启一个后台线程( 因此说redis不是单线程,只是单eventloop而已 ),这个后台线程会每一秒调用一次sync。这里要问了,RDB的时候为何没有考虑sync的事情呢?由于RDB是一次性存储的,不像AOF这样屡次存储,RDB的时候调用一次sync也没什么影响,并且使用bg save的时候,子进程会本身退出(exit),这时候exit函数内会冲刷缓冲区,自动就写入了文件中。
再来看,若是不想使用aof_buf保存每次的修改命令,也可使用aof持久化。redis提供aof_rewrite,即根据现有的数据库生成命令,而后把命令写入aof文件中。 很奇特吧?对,就是这么厉害。进行aof_rewrite的时候,redis变量每一个数据库,而后根据key-value对中value的具体类型,生成不一样的命令,好比是list,则它生成一个保存list的命令,这个命令里包含了保存该list所须要的的数据,若是这个list数据过长,还会分红多条命令,先建立这个list,而后往list里面添加元素,总之,就是根据数据反向生成保存数据的命令。而后将这些命令存储aof文件,这样不就和aof append达到一样的效果了么?
再来看,aof格式也支持后台模式。执行aof_bgrewrite的时候,也是fork一个子进程,而后让子进程进行aof_rewrite,把它复制的数据库写入一个临时文件,而后写完后用新号通知父进程。父进程判断子进程的退出信息是否正确,而后将临时文件改名成最终的aof文件。好了,问题来了。在子进程持久化期间,可能父进程的数据库有更新,怎么把这个更新通知子进程呢?难道要用进程间通讯么? 是否是有点麻烦呢?你猜redis怎么作的?它根本不通知子进程。什么,不通知?那更新怎么办? 在子进程执行aof_bgrewrite期间,父进程会保存全部对数据库有更改的操做的命令(增,删除,改等),把他们保存在aof_rewrite_buf_blocks中,这是一个链表,每一个block均可以保存命令,存不下时,新申请block,而后放入链表后面便可,当子进程通知完成保存后,父进程将aof_rewrite_buf_blocks的命令append 进aof文件就能够了。多么优美的设计,想想本身当初还考虑用进程间通讯,别人直接用最简单的方法就完美的解决了问题,有句话说得真对,越优秀的设计越趋于简单,而复杂的东西每每都是靠不住的。
至于aof文件的载入,也就是一条一条的执行aof文件里面的命令而已。不过考虑到这些命令就是客户端发送给redis的命令,因此redis干脆生成了一个假的客户端, 它没有和redis创建网络链接,而是直接执行命令便可。首先搞清楚,这里的假的客户端,并非真正的客户端,而是存储在redis里面的客户端的信息,里面有写和读的缓冲区,它是存在于redis服务器中的。因此,以下图,直接读入aof的命令,放入客户端的读缓冲区中,而后执行这个客户端的命令便可。这样就完成了aof文件的载入。
整个aof持久化的设计,我的认为至关精彩。其中有不少地方,值得膜拜。
redis另外一个比memcached强大的地方,是它支持简单的事务。事务简单说就是把几个命令合并,一次性执行所有命令。对于关系型数据库来讲,事务还有回滚机制,即事务命令要么所有执行成功,只要有一条失败就回滚,回到事务执行前的状态。redis不支持回滚,它的事务只保证命令依次被执行,即便中间一条命令出错也会继续往下执行,因此说它只支持简单的事务。
首先看redis事务的执行过程。首先执行multi命令,表示开始事务,而后输入须要执行的命令,最后输入exec执行事务。 redis服务器收到multi命令后,会将对应的client的状态设置为REDIS_MULTI,表示client处于事务阶段,并在client的multiState结构体里面保持事务的命令具体信息(固然首先也会检查命令是否可否识别,错误的命令不会保存),即命令的个数和具体的各个命令,当收到exec命令后,redis会顺序执行multiState里面保存的命令,而后保存每一个命令的返回值,当有命令发生错误的时候,redis不会中止事务,而是保存错误信息,而后继续往下执行,当全部的命令都执行完后,将全部命令的返回值一块儿返回给客户。redis为何不支持回滚呢?网上看到的解释出现问题是因为客户程序的问题,因此不必服务器回滚,同时,不支持回滚,redis服务器的运行高效不少。在我看来,redis的事务不是传统关系型数据库的事务,要求CIAD那么很是严格,或者说redis的事务都不是事务,只是提供了一种方式,使得客户端能够一次性执行多条命令而已,就把事务当作普通命令就好了, 支持回滚也就不必了。
咱们知道redis是单event loop的,在真正执行一个事物的时候(即redis收到exec命令后),事物的执行过程是不会被打断的,全部命令都会在一个event loop中执行完。可是在用户逐个输入事务的命令的时候,这期间,可能已经有别的客户修改了事务里面用到的数据,这就可能产生问题。 因此redis还提供了watch命令,用户能够在输入multi以前,执行watch命令,指定须要观察的数据,这样若是在exec以前,有其余的客户端修改了这些被watch的数据,则exec的时候,执行处处理被修改的数据的命令的时候,会执行失败,提示数据已经dirty。 这是如何是实现的呢? 原来在每个redisDb中还有一个dict watched_keys,watched_kesy中dictentry的key是被watch的数据库的key,而value则是一个list,里面存储的是watch它的client。同时,每一个client也有一个watched_keys,里面保存的是这个client当前watch的key。在执行watch的时候,redis在对应的数据库的watched_keys中找到这个key(若是没有,则新建一个dictentry),而后在它的客户列表中加入这个client,同时,往这个client的watched_keys中加入这个key。当有客户执行一个命令修改数据的时候,redis首先在watched_keys中找这个key,若是发现有它,证实有client在watch它,则遍历全部watch它的client,将这些client设置为REDIS_DIRTY_CAS,表面有watch的key被dirty了。当客户执行的事务的时候,首先会检查是否被设置了REDIS_DIRTY_CAS,若是是,则代表数据dirty了,事务没法执行,会当即返回错误,只有client没有被设置REDIS_DIRTY_CAS的时候才可以执行事务。 须要指出的是,执行exec后,该client的全部watch的key都会被清除,同时db中该key的client列表也会清除该client,即执行exec后,该client再也不watch任何key(即便exec没有执行成功也是同样)。因此说redis的事务是简单的事务,算不上真正的事务。
以上就是redis的事务,感受实现很简单,实际用处也不是太大。
redis支持频道,即加入一个频道的用户至关于加入了一个群,客户往频道里面发的信息,频道里的全部client都能收到。
实现也很简单,也watch_keys实现差很少,redis server中保存了一个pubsub_channels的dict,里面的key是频道的名称(显然要惟一了),value则是一个链表,保存加入了该频道的client。同时,每一个client都有一个pubsub_channels,保存了本身关注的频道。当用用户往频道发消息的时候,首先在server中的pubsub_channels找到改频道,而后遍历client,给他们发消息。而订阅,取消订阅频道不够都是操做pubsub_channels而已,很好理解。
同时,redis还支持模式频道。即经过正则匹配频道,若有模式频道p, 1, 则向普通频道p1发送消息时,会匹配p,1,除了往普通频道发消息外,还会往p,1模式频道中的client发消息。注意,这里是用发布命令里面的普通频道来匹配已有的模式频道,而不是在发布命令里制定模式频道,而后匹配redis里面保存的频道。实现方式也很简单,在redis server里面有个pubsub_patterns的list(这里为何不用dict?由于pubsub_patterns的个数通常较少,不须要使用dict,简单的list就行了),它里面存储的是pubsubPattern结构体,里面是模式和client信息,以下所示,一个模式,一个client,因此若是有多个clint监听一个pubsub_patterns的话,在list面会有多个pubsubPattern,保存client和pubsub_patterns的对应关系。 同时,在client里面,也有一个pubsub_patterns list,不过里面存储的就是它监听的pubsub_patterns的列表(就是sds),而不是pubsubPattern结构体。
当用户往一个频道发送消息的时候,首先会在redis server中的pubsub_channels里面查找该频道,而后往它的客户列表发送消息。而后在redis server里面的pubsub_patterns里面查找匹配的模式,而后往client里面发送消息。 这里并无去除重复的客户,在pubsub_channels可能已经给某一个client发过message了,而后在pubsub_patterns中可能还会给用户再发一次(甚至更屡次)。 估计redis认为这是客户程序本身的问题,因此不处理。
总的来看,redis比memcached的功能多不少,实现也更复杂。 不过memcached更专一于保存key-value数据(这已经能知足大多数使用场景了),而redis提供更丰富的数据结构及其余的一些功能。不能说redis比memcached好,不过从源码阅读的角度来看,redis的价值或许更大一点。 另外,redis3.0里面支持了集群功能,这部分的代码尚未研究,后续再跟进。