redis的字符串不是直接用c语言的字符串,而是用了一种称为简单动态字符串(SDS)的抽象类型,并将其做为默认字符串。node
redis中包含字符串值的键值对在底层都是由SDS实现的。redis
1 /* 2 * 保存字符串对象的结构 3 */ 4 struct sdshdr { 5 6 // buf 中已占用空间的长度 7 int len; 8 9 // buf 中剩余可用空间的长度 10 int free; 11 12 // 数据空间 13 char buf[]; 14 };
SDS遵循C字符串以空字符结尾的惯例,可是那1个字节不计算在len中。算法
能够重用C字符串库函数里的函数。数据库
一、常数复杂度获取字符串长度数组
C语言若是要获取字符串的长度,须要从第一个字符开始,遍历整个字符串,直到遍历到\0符号,时间复杂度是O(N),即字符串的长度。缓存
而redis因为已经存储了字符串的长度,所以,时间复杂度是O(1)。安全
这样,避免了获取大字符串长度时时间的缓慢。服务器
二、杜绝缓冲区溢出数据结构
C语言给字符串开辟一个存储空间,若是对此存储空间的使用超过开辟的空间,会致使内存溢出。app
例如使用字符串拼接等方式时,就很容易出现此问题。而若是每次拼接以前都要计算每一个字符串的长度,时间上又要耗费好久。
redis的SDS中内置一个sdscat函数,也是用于字符串的拼接。可是在执行操做以前,其会先检查空间是否足够。
若是free的值不够,会再申请内存空间,避免溢出。
三、减小内存分配次数
C语言的字符串长度和底层数组之间存在关联,所以字符串长度增长时须要再分配存储空间,避免溢出;字符串长度减小时,须要释放存储空间,避免内存泄漏。
redis的sds,主要是经过free字段,来进行判断。经过未使用空间大小,实现了空间预分配和惰性空间释放。
1)空间预分配
当须要增加字符串时,sds不只会分配足够的空间用于增加,还会预分配未使用空间。
分配的规则是,若是增加字符串后,新的字符串比1MB小,则额外申请字符串当前所占空间的大小做为free值;若是增加后,字符串长度超过1MB,则额外申请1MB大小。
上述机制,避免了redis字符串增加状况下频繁申请空间的状况。每次字符串增加以前,sds会先检查空间是否足够,若是足够则直接使用预分配的空间,不然按照上述机制申请使用空间。
1 /* 2 * 对 sds 中 buf 的长度进行扩展,确保在函数执行以后, 3 * buf 至少会有 addlen + 1 长度的空余空间 4 * (额外的 1 字节是为 \0 准备的) 5 * 6 * 返回值 7 * sds :扩展成功返回扩展后的 sds 8 * 扩展失败返回 NULL 9 * 10 * 复杂度 11 * T = O(N) 12 */ 13 sds sdsMakeRoomFor(sds s, size_t addlen) { 14 15 struct sdshdr *sh, *newsh; 16 17 // 获取 s 目前的空余空间长度 18 size_t free = sdsavail(s); 19 20 size_t len, newlen; 21 22 // s 目前的空余空间已经足够,无须再进行扩展,直接返回 23 if (free >= addlen) return s; 24 25 // 获取 s 目前已占用空间的长度 26 len = sdslen(s); 27 sh = (void*) (s-(sizeof(struct sdshdr))); 28 29 // s 最少须要的长度 30 newlen = (len+addlen); 31 32 // 根据新长度,为 s 分配新空间所需的大小 33 if (newlen < SDS_MAX_PREALLOC) 34 // 若是新长度小于 SDS_MAX_PREALLOC 默认1M 35 // 那么为它分配两倍于所需长度的空间 36 newlen *= 2; 37 else 38 // 不然,分配长度为目前长度加上 SDS_MAX_PREALLOC 39 newlen += SDS_MAX_PREALLOC; 40 // T = O(N) 41 newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); 42 43 // 内存不足,分配失败,返回 44 if (newsh == NULL) return NULL; 45 46 // 更新 sds 的空余长度 47 newsh->free = newlen - len; 48 49 // 返回 sds 50 return newsh->buf; 51 }
2)懒惰空间释放
懒惰空间释放用于优化sds字符串缩短的操做
当须要缩短sds的长度时,并不当即释放空间,而是使用free来保存剩余可用长度,并等待未来使用。
当有剩余空间,而有有增加字符串操做时,则又会调用空间预分配机制。
当redis内存空间不足时,会自动释放sds中未使用的空间,所以也不须要担忧内存泄漏问题。
四、二进制安全
SDS 的 API 都是二进制安全的: 全部 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据作任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。
sds考虑字符串长度,是经过len属性,而不是经过\0来判断。
五、兼容部分C语言字符串函数
redis兼容c语言对于字符串末尾采用\0进行处理,这样使得其能够复用部分c语言字符串函数的代码,实现代码的精简性。
列表键的底层之一是链表。(底层也有多是压缩列表)
当列表键包含了许多元素,或者元素是比较长的字符串的时候,就会用到链表做为列表键的底层实现。
一、节点结构
1 /* 2 * 双端链表节点 3 */ 4 typedef struct listNode { 5 6 // 前置节点 7 struct listNode *prev; 8 9 // 后置节点 10 struct listNode *next; 11 12 // 节点的值 13 void *value; 14 15 } listNode;
其中prev指向前一个节点,next指向后一个节点,value存储着节点自己的值。多个listNode组成双向链表,以下图所示:
二、链表结构
1 /* 2 * 双端链表结构 3 */ 4 typedef struct list { 5 6 // 表头节点 7 listNode *head; 8 9 // 表尾节点 10 listNode *tail; 11 12 // 节点值复制函数 13 void *(*dup)(void *ptr); 14 15 // 节点值释放函数 16 void (*free)(void *ptr); 17 18 // 节点值对比函数 19 int (*match)(void *ptr, void *key); 20 21 // 链表所包含的节点数量 22 unsigned long len; 23 24 } list;
链表以下图所示:
redis的链表特性以下:
1)双向:每一个listNode节点带有prev和next指针,能够找到前一个节点和后一个节点,具备双向性。
2)无环:list链表的head节点的prev和tail节点的next指针都是指向null。
3)带表头指针和尾指针:即上述的head和tail,获取头指针和尾指针的时间复杂度O(1)。
4)带链表长度计数器;即list的len属性,记录节点个数,所以获取节点个数的时间复杂度O(1)。
5)多态:链表使用void*指针来保存节点的值,能够经过list的dup、free、match三个属性为节点值设置类型特定函数,因此链表能够用于保存不一样类型的值。
字典,又称符号表、关联数组、映射,是一种保存键值对的抽象数据结构。
每一个键(key)和惟一的值(value)关联,键是独一无二的,经过对键的操做能够对值进行增删改查。
redis中字典应用普遍,对redis数据库的增删改查就是经过字典实现的。即redis数据库的存储,和大部分关系型数据库不一样,不采用B+tree进行处理,而是采用hash的方式进行处理。
字典仍是hash键的底层实现之一。
当hash键包含了许多元素,或者元素是比较长的字符串的时候,就会用到字典做为hash键的底层实现。
redis的字典,底层是使用哈希表实现,每一个哈希表有多个哈希节点,每一个哈希节点保存了一个键值对。
一、哈希表
1 /* 2 * 哈希表 3 * 4 * 每一个字典都使用两个哈希表,从而实现渐进式 rehash 。 5 */ 6 typedef struct dictht { 7 8 // 哈希表数组 9 dictEntry **table; 10 11 // 哈希表大小 12 unsigned long size; 13 14 // 哈希表大小掩码,用于计算索引值 15 // 老是等于 size - 1 16 unsigned long sizemask; 17 18 // 该哈希表已有节点的数量 19 unsigned long used; 20 21 } dictht;
其中,table是一个数组,里面的每一个元素指向dictEntry(哈希表节点)结构的指针,dictEntry结构是键值对的结构;
size表示哈希表的大小,也是table数组的大小;
used表示table目前已有的键值对节点数量;
sizemask一直等于size-1,该值与哈希值一块儿决定一个属性应该放到table的哪一个位置。
大小为4的空哈希表结构以下图(左边一列的图)所示:
二、哈希表节点
1 /* 2 * 哈希表节点 3 */ 4 typedef struct dictEntry { 5 6 // 键 7 void *key; 8 9 // 值 10 union { 11 void *val; 12 uint64_t u64; 13 int64_t s64; 14 } v; 15 16 // 指向下个哈希表节点,造成链表 17 struct dictEntry *next; 18 19 } dictEntry;
其中,key表示节点的键;union表示key对应的值,能够是指针、uint64_t整数或int64_t整数;
next是指向另外一个哈希表节点的指针,该指针将多个哈希值相同的键值对链接在一块儿,避免由于哈希值相同致使的冲突。
哈希表节点以下图(左边第一列是哈希表结构,表节点结构从左边第二列开始)所示:
三、字典
1 /* 2 * 字典 3 */ 4 typedef struct dict { 5 6 // 类型特定函数 7 dictType *type; 8 9 // 私有数据 10 void *privdata; 11 12 // 哈希表 13 dictht ht[2]; 14 15 // rehash 索引 16 // 当 rehash 不在进行时,值为 -1 17 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ 18 19 // 目前正在运行的安全迭代器的数量 20 int iterators; /* number of iterators currently running */ 21 22 } dict;
type用于存放用于处理特定类型的处理函数;
privdata用于存放私有数据,保存传给type内的函数的数据;
rehash是一个索引,当没有在rehash进行时,值是-1;
ht是包含两个项的数组,每一个项是一个哈希表,通常状况下只是用ht[0],只有在对ht[0]进行rehash时,才会使用ht[1]。
完整的字典结构以下图所示:
要将新的键值对加到字典,程序要先对键进行哈希算法,算出哈希值和索引值,再根据索引值,把包含新键值对的哈希表节点放到哈希表数组指定的索引上。
redis实现哈希的代码是:
hash =dict->type->hashFunction(key); index = hash& dict->ht[x].sizemask;
算出来的结果中,index的值是多少,则key会落在table里面的第index个位置(第一个位置index是0)。
其中,redis的hashFunction,采用的是murmurhash2算法,是一种非加密型hash算法,其具备高速的特色。
当两个或者以上的键被分配到哈希表数组的同一个索引上,则称这些键发生了冲突。
为了解决此问题,redis采用链地址法。被分配到同一个索引上的多个节点能够用单链表链接起来。
由于没有指向尾节点的指针,因此老是将新节点加在表头的位置。(O(1)时间)
随着操做进行,哈希表保存的键值对会增长或减小,为了让哈希表的负载因子(load factor)维持在一个合理范围,当一个哈希表保存的键太多或者太少,须要对哈希表进行扩展或者收缩。扩展或收缩哈希表的过程,就称为rehash。
rehash步骤以下:
一、给字典的ht[1]申请存储空间,大小取决于要进行的操做,以及ht[0]当前键值对的数量(ht[0].used)。假设当前ht[0].used=x。
若是是扩展,则ht[1]的值是第一个大于等于x*2的2n的值。例如x是30,则ht[1]的大小是第一个大于等于30*2的2n的值,即64。
若是是收缩,则ht[1]的值是第一个大于等于x的2n的值。例如x是30,则ht[1]的大小是第一个大于等于30的2n的值,即32。
二、将保存在ht[0]上面的全部键值对,rehash到ht[1],即对每一个键从新采用哈希算法的方式计算哈希值和索引值,再放到相应的ht[1]的表格指定位置。
三、当ht[0]的全部键值对都rehash到ht[1]后,释放ht[0],并将ht[1]设置为ht[0],再新建一个空的ht[1],用于下一次rehash。
rehash条件:
负载因子(load factor)计算:
load_factor =ht[0].used / ht[0].size,即负载因子大小等于当前哈希表的键值对数量,除以当前哈希表的大小。
扩展:
当如下任一条件知足,哈希表会自动进行扩展操做:
1)服务器目前没有在执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于1。
2)服务器目前正在在执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于5。
收缩:
当负载因子小于0.1时,redis自动开始哈希表的收缩工做。
redis对ht[0]扩展或收缩到ht[1]的过程,并非一次性完成的,而是渐进式、分屡次的完成,以免若是哈希表中存有大量键值对,一次性复制过程当中,占用资源较多,会致使redis服务停用的问题。
渐进式rehash过程以下:
一、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两张哈希表。
二、将字典中的rehashidx设置成0,表示正在rehash。rehashidx的值默认是-1,表示没有在rehash。
三、在rehash进行期间,程序处理正常对字典进行增删改查之外,还会顺带将ht[0]哈希表上,rehashidx索引上,全部的键值对数据rehash到ht[1],而且rehashidx的值加1。
四、当某个时间节点,所有的ht[0]都迁移到ht[1]后,rehashidx的值从新设定为-1,表示rehash完成。
渐进式rehash采用分而治之的工做方式,将哈希表的迁移工做所耗费的时间,平摊到增删改查中,避免集中rehash致使的庞大计算量。
在rehash期间,对哈希表的查找、修改、删除,会先在ht[0]进行。
若是ht[0]中没找到相应的内容,则会去ht[1]查找,并进行相关的修改、删除操做。而增长的操做,会直接增长到ht[1]中,目的是让ht[0]只减不增,加快迁移的速度。
字典在redis中普遍应用,包括数据库和hash数据结构。
每一个字典有两个哈希表,一个是正常使用,一个用于rehash期间使用。
当redis计算哈希时,采用的是MurmurHash2哈希算法。
哈希表采用链地址法避免键的冲突,被分配到同一个地址的键会构成一个单向链表。
在rehash对哈希表进行扩展或者收缩过程当中,会将全部键值对进行迁移,而且这个迁移是渐进式的迁移。
跳跃表(skiplist)是一种有序的数据结构,它经过每一个节点中维持多个指向其余节点的指针,从而实现快速访问。
跳跃表平均O(logN),最坏O(N),支持顺序遍历查找。
在redis中,有序集合(sortedset)的其中一种实现方式就是跳跃表。
当有序集合的元素较多,或者集合中的元素是比较常的字符串,则会使用跳跃表来实现。
跳跃表是由各个跳跃表节点组成。
1 /* ZSETs use a specialized version of Skiplists */ 2 /* 3 * 跳跃表节点 4 */ 5 typedef struct zskiplistNode { 6 7 // 成员对象 8 robj *obj; 9 10 // 分值 11 double score; 12 13 // 后退指针 14 struct zskiplistNode *backward; 15 16 // 层 17 struct zskiplistLevel { 18 19 // 前进指针 20 struct zskiplistNode *forward; 21 22 // 跨度 23 unsigned int span; 24 25 } level[]; 26 27 } zskiplistNode;
1 /* 2 * 跳跃表 3 */ 4 typedef struct zskiplist { 5 6 // 表头节点和表尾节点 7 struct zskiplistNode *header, *tail; 8 9 // 表中节点的数量 10 unsigned long length; 11 12 // 表中层数最大的节点的层数 13 int level; 14 15 } zskiplist;
上图最左边就是跳跃表的结构:
header和tail:是跳跃表节点的头结点和尾节点,
length:是跳跃表的长度(即跳跃表节点的数量,不含头结点),
level:表示层数中最大节点的层数(不计算表头结点)。
所以,获取跳跃表的表头、表尾、最大层数、长度的时间复杂度都是O(1)。
跳跃表节点:
层:节点中用L1,L2表示各层,每一个层都有两个属性,前进指针(forward)和跨度(span)。每一个节点的层高是1到32的随机数
前进指针:用于访问表尾方向的节点,便于跳跃表正向遍历节点的时候,查找下一个节点位置;
跨度:记录前进指针所指的节点和当前节点的距离,用于计算排位,访问过程当中,将沿途访问的全部层的跨度累计起来,获得的结果就是跳跃表的排位。
后退指针:节点中用BW来表示,其指向当前节点的前一个节点,用于反向遍历时候使用。每次只能后退至前一个节点。
分值:各节点中的数字就是分值,跳跃表中,节点按照分值从小到大排列。
成员对象:各个节点中,o1,o2是节点所保存的成员对象。是一个指针,指向一个字符串对象。
表头节点也有后退指针,分值,成员对象,由于不会被用到,因此图中省略。
分值能够相同,成员对象必须惟一。
分值相同时,按照成员对象的字典序从小到大排。
跨度用来计算排位:
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,而且这个集合的元素数量很少时,Redis就会使用整数集合做为集合键的底层实现。
它能够保存类型为int16_t、int32_t或者int64_t的整数值,而且保证集合中不会出现重复元素。
1 typedef struct intset { 2 // 编码方式 3 uint32_t encoding; 4 // 集合包含的元素数量 5 uint32_t length; 6 // 保存元素的数组 7 int8_t contents[]; 8 } intset;
contents数组是整数集合的底层实现:整数集合的每一个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序地排列,而且数组中不包含任何重复项。
升级:
每当咱们要将一个新元素添加到整数集合里面,而且新元素的类型比整数集合现有全部元素的的类型都要长时,整数集合须要先进行升级,而后才能将新元素添加到整数集合里面。
根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
将底层数组现有的全部元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上(从后往前),并且在放置元素的过程当中,须要继续位置底层数组的有序性质不变。
将新元素添加到底层数组里面。
将encoding属性更改。
整数集合添加新元素的时间复杂度为O(N)。
由于引起升级的元素要么最大要么最小,全部它的位置要么是0要么是length-1。
升级的好处:
提高整数集合的灵活性,能够随意将int16,int32,int64的值放入集合。
尽量地节约内存
降级:
整数集合不支持降级操做
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当一个列表键只包含少许列表项,而且每一个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来作列表键的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩列表有一下几个组成部分:
每一个压缩列表节点能够保存一个字节数组或者一个整数值,而每一个节点都由previous_entry_length、encoding、content三个部分组成。
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。
由于有了这个长度,因此程序能够经过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。
压缩列表的从表尾向表头遍历操做就是使用这一原理实现的。
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度
节点的content属性负责保存节点的值,节点值能够是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
连锁更新:
因为previous_entry_length多是一个或者五个字节,全部插入和删除操做带来的连锁更新在最坏状况下须要对压缩列表执行N次空间重分配操做,而每次空间重分配的最坏复杂度为O(N),全部连锁更新的最坏复杂度为O(N^2)。
但连锁更新的条件比较苛刻,并且压缩列表中的数据量也不会太多,所以不须要注意性能问题,平均复杂度仍然是O(N)。
Redis对象系统中包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
实现了基于引用计数的内存回收机制。
Redis使用对象来表示数据库中的键和值。
/* * 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表示了该对象的对象类型:
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象
SET msg “Hello World”
TYPE msg
输出 string
OBJECT ENCODING msg
输出 embstr
字符串对象的编码能够是int、raw、embstr
若是值是字符串对象,且长度大于32字节,那么编码为raw
若是值是字符串对象,且长度小于等于32字节,那么编码为embstr
embstr的建立只需分配一次内存,而raw为两次,分别建立redisObject结构和sdshdr结构。
相对地,embstr释放内存的次数也由两次变为一次。
embstr的objet和sds放在一块儿,更好地利用缓存带来的优点。
redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改其实是先转换为raw再进行修改。
列表对象的编码能够是ziplist或者linkedlist。
当列表对象同时知足下面两个条件时,则使用ziplist:
全部字符串元素的长度都小于64字节
元素数量小于512
ziplist是一种压缩列表,它的好处是更能节省内存空间,由于它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每一个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。由于为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会从新进行realloc。以下图所示,对象结构中ptr所指向的就是一个ziplist。整个ziplist只须要malloc一次,它们在内存中是一块连续的区域。
linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增长一个node的时候,就须要从新malloc一块内存。
哈希对象的底层实现能够是ziplist或者hashtable。
当列表对象同时知足下面两个条件时,则使用ziplist:
全部键值对的键和值的字符串度都小于64字节
键值对数量小于512
集合对象的编码能够是intset或者hashtable。
知足下面两个条件,使用intset:
因此有元素都是整数值
元素数量不超过512个
有序集合的编码可能两种,一种是ziplist,另外一种是skiplist与dict的结合。
dict字典为有序集合建立了一个成员到分值的映射。给一用O(1)的时间查到分值。
当有序集合对象同时知足下面两个条件时,则使用ziplist:
全部元素的字符串度都小于64字节
元素数量小于128
默认下,Redis客户端的目标数据库为0号数据库。
SELECT 2 能够切换到2号数据库
经过EXPIRE或者PEXPIRE,能够以秒或者毫秒为键设置生存时间。服务器会自动删除生存时间为0的键。
数据库主要由dict和expires两个字典构成,其中dict负责保存键值对,expires负责保存键的过时时间。
服务器中的非空数据库以及他们的键值对统称为数据库状态。
RDB持久化能够将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB能够手动执行,也能够按期执行。能够将某个时间点上的数据库状态保存到RDB文件中。经过该文件也能够还原数据库状态。
RDB文件是一个通过压缩的二进制文件,由多个部分组成。
对于不一样类型的键值对,RDB文件会使用不一样的方式来保存它们。
有两个命令能够生成Redis文件,SAVE和BGSAVE。
SAVE会阻塞服务器进程,直到RDB建立完成,期间不能处理任何命令请求。
BGSAVE会派生出一个子进程,由子进程建立RDB,父进程能够继续处理其余命令请求。
BGSAVE执行时,客户端发送的SAVE、BGSAVE这两个命令会被服务器拒绝,BGREWRITEAOF会被延迟到BGSAVE执行完毕后执行。
服务器启动时检测到RDB文件存在,就会自动载入RDB文件。
若是服务器开启了AOF持久化功能,服务器会优先使用AOF文件来还原数据库状态。
用户能够经过save选项设置多个保存条件,只要一个知足,服务器就会执行BGSAVE
save 900 1 ,900秒内对数据库至少进行了1次修改
svae 300 10,300秒内对数据库至少进行了10次修改
AOF(append only file)持久化是经过保存Redis服务器所执行的写命令来记录数据库状态。
实现可分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
命令追加:
服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
AOF文件的写入与同步:
服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到缓冲区里,因此在每次结束一个事件循环以前,会考虑是否将缓冲区的内容写入到AOF文件里。
Redis能够建立一个新的AOF文件来替代现有的AOF文件,虽然数据库状态相同,但新的AOF文件不会包含任何浪费空间的冗余命令,新文件体积会小不少。
实现:不是读取现有AOF文件,而是根据现有数据库状态,用最少的命令去获得这个状态。
一个Redis集群一般由多个节点组成,刚开始时,每一个节点是相互独立的。咱们必须将各个独立的节点链接起来。
节点经过握手来将其余节点添加到本身所处的集群中。
127.0.0.1:7000>CLUSTER MEET 127.0.0.1 7001 能够将7001添加到节点7000所在的集群里。
Redis集群经过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个槽(slot)。
数据库中每一个键都属于其中一个槽,每一个节点能够处理0-16384个槽。
当16384个槽都有节点在处理时,集群处于上线状态,只要有一个槽没有获得处理,那么集群处于下线状态。
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 能够将槽0-5000指派给节点7000负责。
每一个节点都会记录哪些槽指派给了本身,哪些槽指派给了其余节点。
客户端向节点发送键命令,节点要计算这个键属于哪一个槽。
若是是本身负责这个槽,那么直接执行命令,若是不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
从新分片操做能够将任意数量已经指派给某个节点的槽改成指派给另外一个节点。
从新分片期间可能会出现这种状况:属于迁移槽的一部分键值对保存在源节点里,而另外一部分保存在目标节点里。
若是节点A正在迁移槽i到节点B,那么当节点A没能在本身数据库中找到命令指定的数据库键时,节点会向客户端返回一个ASK错误,指引客户端到节点B继续查找。
Redis集群的节点分为主节点和从节点。
主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线后,替代主节点。
集群中的每一个节点会按期向其余节点发送PING消息,以此来检测对方是否在线。
Redis的发布订阅由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。
SUBSCRIBE:客户端能够订阅一个或多个频道,成为这些频道的订阅者。每当有客户端向这些频道发消息的时候,频道的全部订阅者均可以收到这条消息。
PSUBSCRIBE:客户端能够订阅一个或多个模式,成为这些模式的订阅者。每当有客户端向这些频道发消息的时候,订阅频道以及与这个频道相匹配的模式的订阅者都会收到消息。
Redis将全部频道的订阅关系都保存在服务器状态的pubsub_channels字典里,键是被订阅的频道,值是一个链表,记录了全部订阅这个频道的客户端。
UNSUBSCRIBE用于退订频道。
Redis将全部模式的订阅关系都保存在服务器状态的pubsub_patterns链表里。
链表节点中记录了被订阅的模式以及订阅这个模式的客户端。
PUNSUBSCRIBE用于退订模式。
PUBLISH 频道 消息 ,能够将消息发送给频道。
频道以及与频道相匹配的模式的订阅者都会收到消息。
PUBSUB NUMSUB 【channel-1 ... channel-n】 接受任意多个频道做为输入参数,返回这些频道的订阅者数量。
PUBSUB NUMPAT ,返回服务器当前被订阅模式的数量。
Redis经过MULTI、EXEC、WATCH等命令来实现事务功能。
事务提供了一种将多个命令请求打包,而后一次性、按顺序执行多个命令的机制。
事务在执行期间,服务器不会中断事务去执行其余命令。
事务首先以MULTI开始,接着多个命令放入事务中,最后由EXEC命令将这个事务提交。
MULTI命令能够将执行该命令的客户端从非事务状态切换至事务状态。
切换到事务状态后,若是客户端发送的命令为EXEC、DISCARD、WATCH、MULTI,那么服务器会当即执行,其余命令则会放入事务队列里。
处于事务状态时,EXEC会被当即执行。服务器会遍历事务队列,执行队列中的全部命令,最后将结果返回给客户端。
WATCH命令是一个乐观锁,它能够在EXEC命令执行以前,监视任意数量的数据库键,并在EXEC执行时,检查被监视的键是否至少有一个被修改过了,若是是,那么服务器将拒绝执行事务。
每一个Redis数据库都保存着一个watched_keys字典,这个字典的键是被WATCH监视的键,值是一个链表,记录了全部监视相应数据库键的客户端。
若是某个键被修改,那么监视该键的客户端的REDIS_DIRTY_CAS标识就会被打开。
执行EXEC时,服务器会根据客户端的REDIS_DIRTY_CAS标识是否被打开来决定是否执行事务。
Redis的事务和传统的关系型事务最大的区别在于,Redis不支持事务回滚机制,即便事务队列中的某个命令在执行期间出现错误,整个事务也会继续执行下去。
若是一个事务在入队命令过程当中,出现了命令不存在或者命令格式错误,那么Redis将拒绝执行这个事务。
事务执行过程当中,出错的命令不会对数据库作任何修改。
只有当服务器运行在AOF持久化模式下,而且appendfsync为always时,这种配置的事务才具备持久性。