《Redis设计与实现》读书笔记
前 言
(1)什么是redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。简而言之redis就是放在远程网络上的一个key-value数据结构。
(2)redis有什么作用
Redis可以用来做cache,也可以用来做持久化db,甚至用来做队列。队列就是存储临时性数据,而cache主要存储那些用户频繁使用的数据。当然也可以来做db,但是实现持久化存储需要付出昂贵的代价。
(3)redis有什么优缺点
Redis的优点
--redis丰富的数据结构----更加方便操作
比如队列的先进先出,hash结构O(1)的快速查询,sorted-set有序集合方便获取排名
--redis的数据都是在内存操作---更加快速操作
---单线程处理---避免了锁
---非阻塞式IO多路复用---充分利用网络IO
---设计简单,效率快
---方便扩展,不需要像mysql一样分库分表,只需要简单扩大内存,或者增加redis的个数
Redis的缺点
Rdb备份不具备实时性
Aof持久化比较消耗内存和cpu,甚至当aof文件过大的时候占用磁盘
(4)你可以从redis中收获哪些东西
---更好的使用redis
---借鉴redis的网络模型
---借鉴redis的内存编码和数据结构设计
---借鉴redis的编码风格
第一部分: 数据结构与对象
1. SDS
1.1 应用
list对象当列表比较长,而且存储的内容也比较长的时候,采用此种数据结构编码
在redis中应用有:列表键,发布与订阅,慢查询,监视器,多个客户端,客户端的输出缓冲区
1.2 数据结构
struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[]; // 依然以’\0’结尾 };
1.3 设计原则--- SDS与C字符串的区别
l 常数复杂度获取字符串长度-----sds->len
l 杜绝缓冲区溢出-----通过free的长度来判断是否有充足的空间
l 减少修改字符串时带来的内存重分配次数----free够的时候,不需要重分配,只有free不足的时候需要扩展。如果对字符串收缩也不需要立刻回收空间,只需要修改len和free的属性即可。----空间预分配和惰性空间释放
l 兼容部分C字符串函数----由于是以”\0”结尾
l 二进制安全。
SDS相对C字符串虽然有些操作更快更便捷,但是这是以事先预分配超过需求的空间,以及记录len和free来完成的。这个过程有点以牺牲空间换时间的方法。而且这个len和free大小设置也是一门技术活!
1.4 应用
Key-value存储系统中的key基本采用的是sds的数据结构,字符串对象,客户端的输入缓存区存储的字符串就是sds对象。
1.5 常用操作
get:sdsrange---O(n)
set:sdscpy—O(n)
create:sdsnew---O(1)
len:sdslen---O(1)
1.6 源码
源码参见: src/sds.h, src/sds.c
2. 链表
2.1 数据结构
typedef struct listNode { // 前置节点 struct listNode *prev;
// 后置节点 struct listNode *next;
// 节点的值 void *value; } listNode; typedef struct list { // 表头节点 listNode *head;
// 表尾节点 listNode *tail;
// 节点值复制函数 void *(*dup)(void *ptr);
// 节点值释放函数 void (*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr, void *key); // 链表所包含的节点数量 unsigned long len; } list;
2.2 特点
方便插入删除,方便重排,特别适合队列这种频繁的push和pop的操作
2.3 常用操作
rpush: listAddNodeHead ---O(1)
lpush: listAddNodeTail ---O(1)
push:listInsertNode ---O(1)
index : listIndex ---O(N)
pop:ListFirst/listLast ---O(1)
llen:listLength ---O(N)
2.4 源码参见:
src/adlist.h, src/adlist.c
3. 字典
3.1 应用
在redis中的应用主要有db(数据存储在字典中),哈希键
3.2 数据结构
(1)字典
typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;
(2)hash表
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
(3)哈希节点
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next;
} dictEntry; /* * 字典类型特定函数 */ typedef struct dictType { // 计算哈希值的函数 unsigned int (*hashFunction)(const void *key); // 复制键的函数 void *(*keyDup)(void *privdata, const void *key); // 复制值的函数 void *(*valDup)(void *privdata, const void *obj); // 对比键的函数 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 销毁键的函数 void (*keyDestructor)(void *privdata, void *key); // 销毁值的函数 void (*valDestructor)(void *privdata, void *obj); } dictType;
计算hash值和索引值的方法
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask
3.3 渐进式Rehash
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让hash表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对hash表的大小进行相应的扩展活放缩。
hash表的负载因子 = hash节点个数/hash表的长度
为了避免rehash(hash节点个数可能成百上千万个)对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1], 而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1].
渐进式rehash的基本步骤是:
(1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
(2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,标识rehash开始。
(3)在rehash进行期间,每次对字典执行添加、删除、查找、删除或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1], 当rehash工作完成之后,程序将rehashidx属性的值增一。
(4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx的属性的值设为-1,表示rehash操作已完成。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到字典的每个添加删除查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。
最后来一句非常注明的话语——talk is cheap,show me the code.
int dictRehash(dict *d, int n) { // 只可以在 rehash 进行中时执行 if (!dictIsRehashing(d)) return 0; // 进行 N 步迁移T = O(N) while(n--) { dictEntry *de, *nextde; // 如果 0 号哈希表为空,那么表示 rehash 执行完毕 if (d->ht[0].used == 0) { // 释放 0 号哈希表 zfree(d->ht[0].table); // 将原来的 1 号哈希表设置为新的 0 号哈希表 d->ht[0] = d->ht[1]; // 重置旧的 1 号哈希表 _dictReset(&d->ht[1]); // 关闭 rehash 标识 d->rehashidx = -1; // 返回 0 ,向调用者表示 rehash 已经完成 return 0; } // 确保 rehashidx 没有越界 assert(d->ht[0].size > (unsigned)d->rehashidx); // 略过数组中为空的索引,找到下一个非空索引 while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++; // 指向该索引的链表表头节点 de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ // 将链表中的所有节点迁移到新哈希表T = O(1) // 获取rehashidx索引上的所有键值将其移植到ht[1]的hashtable while(de) { unsigned int h; // 保存下个节点的指针 nextde = de->next; /* Get the index in the new hash table */ // 计算新哈希表的哈希值,以及节点插入的索引位置 h = dictHashKey(d, de->key) & d->ht[1].sizemask; // 插入节点到新哈希表 de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; // 更新计数器 d->ht[0].used--; d->ht[1].used++; // 继续处理下个节点 de = nextde; } // 将刚迁移完的哈希表索引的指针设为空 d->ht[0].table[d->rehashidx] = NULL; // 更新 rehash 索引 d->rehashidx++; } return 1; }
3.4 使用场景及其特点
字典应用比较广泛,key是字符串对象,value可以是任意对象,比如字符串对象,列表对象,集合对象,hash表对象等
3.5 常用操作
Hset: dictAdd/dictReplace ----O(1)
Hget: dictFetchValue---O(1)
Randomkey:dictGetRandomKey---O(1)
Hdel/del: dictDelete---O(1)
3.6 源码
src/dict.h, src/dict.c
4. 跳跃表(非常有意思)
跳跃表(skiplist)是一种有序数据结构,它通过在某个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
4.1 应用
redis中的应用:有序集合键,集群节点中用作内部数据结构
4.2 数据结构
/* * 跳跃表节点 */ typedef struct zskiplistNode { // 成员对象 robj *obj; // 分值 double score; // 后退指针 struct zskiplistNode *backward; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度---前进指针所指向节点与当前节点的距离 unsigned int span; } level[]; } zskiplistNode; /* * 跳跃表 */ typedef struct zskiplist { // 表头节点和表尾节点 struct zskiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
4.3 特点
类似红黑树,但是实现较红黑树简单。排序或者有序查找可以达到二分查找的效率,该数据结构是以空间换时间。
4.4 操作
zadd---zslinsert---平均O(logN), 最坏O(N)
zrem---zsldelete---平均O(logN), 最坏O(N)
zrank--zslGetRank---平均O(logN), 最坏O(N)
zrange
/* * 创建一个成员为 obj ,分值为 score 的新节点, * 并将这个新节点插入到跳跃表 zsl 中。 * 函数的返回值为新节点。 * T_wrost = O(N^2), T_avg = O(N log N) */ zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; unsigned int rank[ZSKIPLIST_MAXLEVEL]; int i, level; redisAssert(!isnan(score)); // 在各个层查找节点的插入位置 // T_wrost = O(N^2), T_avg = O(N log N) x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { /* store rank that is crossed to reach the insert position */ // 如果 i 不是 zsl->level-1 层 // 那么 i 层的起始 rank 值为 i+1 层的 rank 值 // 各个层的 rank 值一层层累积 // 最终 rank[0] 的值加一就是新节点的前置节点的排位 // rank[0] 会在后面成为计算 span 值和 rank 值的基础 rank[i] = (i == (zsl->level-1)) ? 0 : rank[i+1]; // 沿着前进指针遍历跳跃表 // T_wrost = O(N^2), T_avg = O(N log N) while (x->level[i].forward && (x->level[i].forward->score < score || // 比对分值 (x->level[i].forward->score == score && // 比对成员, T = O(N) compareStringObjects(x->level[i].forward->obj,obj) < 0))) { // 记录沿途跨越了多少个节点 rank[i] += x->level[i].span; // 移动至下一指针 x = x->level[i].forward; } // 记录将要和新节点相连接的节点 update[i] = x; } /* we assume the key is not already inside, since we allow duplicated * scores, and the re-insertion of score and redis object should never * happen since the caller of zslInsert() should test in the hash table * if the element is already inside or not. * * zslInsert() 的调用者会确保同分值且同成员的元素不会出现, * 所以这里不需要进一步进行检查,可以直接创建新元素。 */ // 获取一个随机值作为新节点的层数 // T = O(N) level = zslRandomLevel(); // 如果新节点的层数比表中其他节点的层数都要大 // 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中 // 将来也指向新节点 if (level > zsl->level) { // 初始化未使用层 // T = O(1) for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } // 更新表中节点最大层数 zsl->level = level; } // 创建新节点 x = zslCreateNode(level,score,obj); // 将前面记录的指针指向新节点,并做相应的设置 // T = O(1) for (i = 0; i < level; i++) { // 设置新节点的 forward 指针 x->level[i].forward = update[i]->level[i].forward; // 将沿途记录的各个节点的 forward 指针指向新节点 update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ // 计算新节点跨越的节点数量 x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); // 更新新节点插入之后,沿途节点的 span 值 // 其中的 +1 计算的是新节点 update[i]->level[i].span = (rank[0] - rank[i]) + 1; } /* increment span for untouched levels */ // 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点 // T = O(1) for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } // 设置新节点的后退指针,update[0]是目的结点 x->backward = (update[0] == zsl->header) ? NULL : update[0]; if (x->level[0].forward) x->level[0].forward->backward = x; else zsl->tail = x; // 跳跃表的节点计数增一 zsl->length++; return x; }
4.5 源码
src/zset.c
5. 整数集合
5.1 应用
整数集合是集合键的底层实现之一, 当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
5.2 数据结构
typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; } intset;
5.3 常用操作
sadd:intsetAdd---O(1)
smembers:intsetGetO(1)---O(N)
srem:intsetRemove---O(N)
slen:intsetlen ---O(1)
5.4 源码
src/intset.h, src/intset.c
6. 压缩列表---内存编码的数据结构
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
6.1 应用
压缩列表是列表键和hash键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
6.2 数据结构
Ziplist的数据结构
Zlbytes |
Zltail |
Zlen |
Entry1 |
Entry2 |
… |
entryN |
Zlend |
Entry的数据结构
previous_entry_length |
encoding |
content |
/* * 保存 ziplist 节点信息的结构 */ typedef struct zlentry { // prevrawlen :前置节点的长度 // prevrawlensize :编码 prevrawlen 所需的字节大小 unsigned int prevrawlensize, prevrawlen; // len :当前节点值的长度 // lensize :编码 len 所需的字节大小 unsigned int lensize, len; // 当前节点 header 的大小 // 等于 prevrawlensize + lensize unsigned int headersize; // 当前节点值所使用的编码类型 unsigned char encoding; // 指向当前节点的指针 unsigned char *p; } zlentry;
6.3 常用操作
Push/hset:ziplistPush/ziplistInsert 平均O(N), 最坏O(N2)
Llen/hlen:ziplislen平均O(N), 最坏O(N2)
Hget:ziplistGet ----O(1)
Index:ziplistIndex----O(N)
Hdel/pop:ZiplistDelete---平均O(N), 最坏O(N2)
遍历:ziplistPrev/ziplistNext----O(1)
6.4 源码
src/ziplist.c src/siplist.h
7. Redis对象
7.1 应用
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据库建立了一个对象系统,比如字符串对象,列表对象,集合对象,hash表对象,有序集合对等
使用对象的好处在于:
(1)我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率—对应redisObject中的encoding
(2)Redis的对象还实现了基于引用计数计数的内存回收机制---对应redisObject中的refCount
(3)Redis还通过引用计数实现了对象共享机制
(4)Redis对象带有访问时间记录信息,该信息可以计算数据库键的空转时长,从而确定在内存不足的情况下是否优先删除
7.2 数据结构
typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用计数 int refcount; // 指向实际值的指针 void *ptr; } robj;
7.3 对象编码---对象采用何种数据结构作为对象底层实现
每种类型对象都有哪些编码,什么时候选择何种编码
字符串对象编码
字符串对象编码:REDIS_ENCODING_INT, REDIS_ECODING_EMBSTR,REDIS_ENCODING_RAW. 如果这个string是整数值,其编码就采用REDIS_ENCODING_INT的形式。如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构。而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。正因为此,所以embstr编码比raw编码在保存短字符串方面更加高效(embstr比raw少一次分配,少一次释放,而且embstr的查找连续内存更加高效)。
int编码和embstr编码的字符串对象在条件满足的情况下,会被转换为raw的编码的字符串编码。如果int转化为字符串的时候,这个时候就会采用raw编码,如果embstr编码的字符串长度变长的时候,embstr会转化为raw编码。
列表对象编码
列表对象的编码包括ziplist和linkedlist。ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素。当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
列表对象保存的所有字符串元素的长度都小于64个字节;
列表对象保存的元素数量少于512个;
不能同时满足这两个条件的列表对象需要使用linkedlist
如何进行编码转换,list的默认编码是ziplist,对象操作时一旦不能同时满足以上两个条件,就转化编码。
集合对象编码
集合对象的编码可以是intset或者hashTable。
当集合对象保存的所有元素都是整数值;集合对象保存的元素数量不超过512个的时候,此时采用hashTable的编码方式。
哈希对象编码
哈希对象的编码可以是ziplist(压缩链表)或者hashtable(字典)。当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
哈希对象保存的键值对数量小于512个;
如果上述两个条件不能同时满足的时候,hash对象采用REDIS_ENCODING_HT的编码方式。
有序集合对象编码
有序集合zset的编码方式为skiplist或者ziplist。
zset同时采用了skiplist和dict两个数据结构来实现有序集合。Skiplist有利于有序查找,而dict有利于范围查找。因为他们会共享元素的成员和分值,故并不会造成任何数据重复,只是多了一些指针,所以不会照成内存浪费。
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
有序集合保存的元素数量小于128个;
有序集合保存的所有元素成员的长度都小于64个字节;
不能满足以上两个条件的有序集合对象将使用skiplist编码。
7.4 对象操作---类型检查与命令多态
在通过命令进行对象操作的时候,首先要获取该key所对应的对象类型,进行对象类型检查,判断该对象类型是否支持这个命令。有些命令支持多个数据类型,如type命令,有些命令只支持特定数据类型,比如lpush只支持list对象。做了类型检查之后,还需要判断是否需要进行编码转换,之后再获取编码,根据编码调用相应的数据结构的底层操作函数。(类型检查-->编码转换-->编码变换)
7.5 对象的空转时长
对象有一个空转时长的lru属性,该属性记录了对象最后一次被命令程序访问的时间。OBJECT IDLETIME命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间计算出来的。
当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的部分键会优先被服务器释放,从而回收内存。
7.6 对象的引用计数---内存回收 & 对象共享
因为C语言并不具备自动内存回收功能,所以redis对象系统构建了一个引用计数refcount技术来实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增1
- 当对象不再被一个程序使用时,它的引用计数会减1
- 当对象的引用计数为0时,对象所占用的内存会被释放。
7.7 特点
7.8 总结
上图描述了各类型对象从数据结构到对象的过程。
第二部分 单机数据库
8. 单机数据库
服务器中的多个数据库是server中的redisDB *Db和dbnums属性决定的。
可以通过select n来选择n号数据库,比如select 0来选择0号数据库。注意在flushdb操作之前一定要select切换到目标数据库。
8.1 数据库的数据结构
/* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ typedef struct redisDb { // 数据库键空间,保存着数据库中的所有键值对 dict *dict; /* The keyspace for this DB */ // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳 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 */ // 正在被 WATCH 命令监视的键 dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */ // 数据库号码 int id; /* Database ID */ // 数据库的键的平均 TTL ,统计信息 long long avg_ttl; /* Average TTL, just for stats */ } redisDb;
8.2 数据库的增删查改
数据库中进行增删查改,主要是对redis对象进行增删查改,以redis的List对象为例子:
添加:rpush key value
删除:pop key
查找:lindex key idx
修改:lset key value
长度:llen key
如果是列表对象:
redis和mysql数据库不同的是因为拥有丰富的数据类型,所以其增删查改不像mysql关系数据库操作那么单一(insert/del/select/update)。
8.3 数据库中过期键设置和删除
通过expire命令或者pexpire命令,客户端可以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL)。除此之外,客户端还可以通过expireat命令或pexpireat命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time—UNIX时间戳)。
可以通过persist命令移除某个键的过期时间。
过期键的删除策略:
定时删除:在设置过期键的同时创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。(内存友好,耗cpu)
惰性删除:放任过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,如果没有过期,则保留。(cpu友好,占内存)
定期删除:每隔一段时间,程序会对数据库进行一次检查,或者对过期字典检查,删除里面的过期键。至于要隔多久,一次检查多少数据库,删除多少过期键,则由算法决定。
为了主从一致,从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来del命令,这种统一,中心化的过期键删除策略可以保证主从服务器数据的一致性。
8.4 数据库的RDB备份
Redis可以通过RDB文件保存数据库状态,也可以通过RDB文件恢复数据库状态。可以通过save或者bgsave命令来保存RDB文件。Bgsave是启动一个子进程在后台进行运行。
RDB文件的数据结构如下所示:
REDIS |
db_version |
databases |
EOF |
check_sum |
而databases的数据结构如下所示
SELECTEDB |
db_number |
key_value_pairs |
key_value_pairs的数据结构如下所示
EXPIRETIME_MS |
ms |
TYPE |
key |
value |
每种类型的value又有不同的数据结构
(1) 字符串对象
len |
String |
(2) 列表对象
list_length |
item1 |
item2 |
…. |
itemN |
列表项item是字符串对象,所以按照上面字符串对象的数据结构进行构造。
比如:
3 |
5 |
“Tsing” |
2 |
“Lo” |
7 |
“lqtsing” |
(3) 集合对象
set_size |
elem1 |
elem2 |
…. |
elemN |
(4) 哈希表对象
hash_size |
key_value_pair_1 |
key_value_pair_2 |
…. |
key_value_pair_N |
Hash表对象结点key-value的结构
Key1 |
Value1 |
Key2 |
Value2 |
Key3 |
Value3 |
… |
8.5 数据库的aof重写
Rdb文件是保存数据,而aof文件主要是记录数据写入修改删除操作。
Aof文件主要分为三步
(1) 命令追加:将命令追加到aof_buf缓冲区中
(2) 写入文件:将其写入aof文件,此时有一部分暂时保存在一个内存缓冲区
数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。所以当没有到指定时限且缓冲区未满的时候,此时有一部分还停留在内存缓冲区。
(3) 文件同步:将保存在内存缓冲区那部分强制写入硬盘。
Aof文件的格式:
*argc $argv[i]_len $argv[i]
第一个是*+命令参数个数,第二个是$+第一个参数的长度,第三个是第一个参数的值,第四个是$+第二个参数的长度,第五个是第二个参数的值,… 第2n是$+第n个参数的字符串长度,第2n+1是第n个参数的字符串值,每个都以换行符分隔。
比如 set name tsing这个命令在aof的记载如下
*3\r\n$3\r\nset\r\n$4\r\nname\r\n$5\r\ntsing\r\n
可以将aof文件以pipeline的方式
cat a.aof | redis-cli –h 172.16.xx.xx –p 6379 –pipe 通过aof文件恢复redis的数据库状态。
为了避免aof文件太过臃肿冗余,还会对aof文件进行相关的重写。
9. Redis事务驱动(事件,客户端,服务器)
9.1 Redis事务驱动
Redis服务器是一个事件驱动程序。
文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
简而言之:Redis服务主要是通过监听和处理各种事件来完成各种任务。包括客户端连接,处理客户端请求,返回客户端请求结果,关闭连接,定时备份,定时清理内存,定时关闭连接,定时清理过期键等操作。
Redis文件事件通过使用I/O多路复用程序来监听多个套接字,文件事件处理器即实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性。
首先我们找到主函数Main来看看redis服务的整个过程, 主函数的代码如下:
int main(int argc, char **argv) { …. // 初始化服务器 initServerConfig(); … // 载入配置文件, options 是前面分析出的给定选项 loadServerConfig(configfile,options); … // 创建并初始化服务器数据结构 initServer(); … // 从 AOF 文件或者 RDB 文件中载入数据 loadDataFromDisk(); … aeMain(server.el); … aeDeleteEventLoop(server.el); … return 0; }
(1) initServerConfig:初始化服务器的相关配置
(2) loadServerConfig:加载服务器的配置文件
- initServer:创建并初始化服务器的数据结构,并且注册文件事件,并且将该事件关联连接应答器,用于监听客户端的连接。
- 当监听到客户端连接acceptTCPHandler,就会创建客户端,并且注册事件并关联客户端命令处理器readQureyFromClient。
- 当监听到有命令输入的时候,服务器会进行命令处理,之后解析命令输入proccessLineBuffer,获取命令参数argv和参数个数argc,根据第一个命令参数argv[0]查找commandTable获取到command的处理函数proc。
- 调用c->cmd->proc会对命令进行处理,根据命令参数读写db的key-value。
- 在返回命令回复addReplay的时候会注册事件关联命令回复起sendReplyToClient。
具体流程如下图所示:
Redis服务(文件事件循环)的具体调用过程
(3) loadDataFromDisk:加载rdb文件或者aof文件
(4) aeMain:事件循环,通过IO多路复用监听各种事件,并且通过apiPoll中的epoll_wait获取需要执行的事件,判断事件类型是读事件还是写事件,如果是读事件,就调用读事件相应的处理器,如果是写事件,就调用写事件相应的处理器。之后处理相应的时间事件。
// 事件处理器的主循环 void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { // 如果有需要在事件处理前执行的函数,那么运行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); // 开始处理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
主函数以及事件循环的执行过程如下:
9.2 文件事件
文件事件时对套接字操作的抽象,每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。同时会对这些文件事件关联一些事件处理器,这些事件处理器定义了某个事件发生时,服务器应该执行的动作。比如初始化服务器将连接应答处理器和事件进行关联,当监听到AE_READABLE事件,此时执行关联的处理器的读事件rfileProc---即连接应答处理。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现,但是IO多路复用程序总是会将所有产生事件的套接字放在一个队列中,然后通过这个队列,以有序、同步、每次以一个套接字的方式向文件事件分派器传送套接字。
文件事件结构:
typedef struct aeFileEvent { // 监听事件类型掩码, // 值可以是 AE_READABLE 或 AE_WRITABLE , // 或者 AE_READABLE | AE_WRITABLE int mask; /* one of AE_(READABLE|WRITABLE) */ // 读事件处理器 aeFileProc *rfileProc; // 写事件处理器 aeFileProc *wfileProc; // 多路复用库的私有数据 void *clientData; } aeFileEvent;
aeCreateFileEvent其实是一个文件事件分派器,将IO多路复用程序监听的套接字fd与各种文件处理器proc进行关联。各种文件处理器定义要执行的各种不同任务,即对套接字各种读写进行相应的操作。
/* * 根据 mask 参数的值,监听 fd 文件的状态, * 当 fd 可用时,执行 proc 函数 */ int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData) { if (fd >= eventLoop->setsize) { errno = ERANGE; return AE_ERR; } if (fd >= eventLoop->setsize) return AE_ERR; // 取出文件事件结构 aeFileEvent *fe = &eventLoop->events[fd]; // 监听指定 fd 的指定事件 if (aeApiAddEvent(eventLoop, fd, mask) == -1) return AE_ERR; // 设置文件事件类型,以及事件的处理器 fe->mask |= mask; if (mask & AE_READABLE) fe->rfileProc = proc; if (mask & AE_WRITABLE) fe->wfileProc = proc; // 私有数据 fe->clientData = clientData; // 如果有需要,更新事件处理器的最大 fd if (fd > eventLoop->maxfd) eventLoop->maxfd = fd; return AE_OK; }
redis服务器的整个过程如图所示,
9.3 时间事件
时间事件应用实例:serverCron函数,它的主要工作包括:
l 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况
l 清理数据库中的过期键值对
l 关闭和清理连接失效的客户端
l 尝试进行AOF和RDB持久化操作
l 如果服务器时主服务器,那么对从服务器要定期同步
l 如果处于集群模式,对集群进行定期同步和连接测试
9.4 服务器和客户端
9.5 Epoll的相关测试(附加相关脚本以及相关流程图)
这个脚本简单的描述服务器等待客户端的连接,连接成功后等待客户端的命令请求,服务器接收和读取客户端的命令请求并等待将处理结果写回给客户端。整个过程与redis的文件事件循环处理机制非常相似,有兴趣的同学可以改写RecvData的函数,将读取的数据进行解析并且经过一定的运算处理,将处理结果调用SendData返回给客户端。
http://itlab.idcquan.com/linux/kernel/894754.html
什么时候中断连接也是一个问题!
还有很多人说redis是单线程处理,这个我在redis中的代码中怎么没有看出来
将上述过程调用绘制出一个流程图,并且简单介绍下epoll
10. 主从复制以及集群机制
11. Mysql VS redis
12. Redis的info和配置文件
13. Redis的编译过程
参考资料:
http://blog.codingnow.com/2014/03/mmzb_redis.html
http://blog.nosqlfan.com/html/4218.html
http://blog.nosqlfan.com/html/4153.html
http://www.infoq.com/cn/articles/tq-redis-memory-usage-optimization-storage
http://blog.jobbole.com/44476/