第2章 简单动态字符串(SDS)
redis的字符串不是直接用c语言的字符串,而是用了一种称为简单动态字符串(SDS)的抽象类型,并将其作为默认字符串。
redis中包含字符串值的键值对在底层都是由SDS实现的。
2.1 SDS定义
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字符串库函数里的函数。
2.2 SDS与C语言字符串的区别
1、常数复杂度获取字符串长度
C语言如果要获取字符串的长度,需要从第一个字符开始,遍历整个字符串,直到遍历到 符号,时间复杂度是O(N),即字符串的长度。
而redis由于已经存储了字符串的长度,因此,时间复杂度是O(1)。
这样,避免了获取大字符串长度时时间的缓慢。
2、杜绝缓冲区溢出
C语言给字符串开辟一个存储空间,如果对此存储空间的使用超过开辟的空间,会导致内存溢出。
例如使用字符串拼接等方式时,就很容易出现此问题。而如果每次拼接之前都要计算每个字符串的长度,时间上又要耗费很久。
redis的SDS中内置一个sdscat函数,也是用于字符串的拼接。但是在执行操作之前,其会先检查空间是否足够。
如果free的值不够,会再申请内存空间,避免溢出。
3、减少内存分配次数
C语言的字符串长度和底层数组之间存在关联,因此字符串长度增加时需要再分配存储空间,避免溢出;字符串长度减少时,需要释放存储空间,避免内存泄漏。
redis的sds,主要是通过free字段,来进行判断。通过未使用空间大小,实现了空间预分配和惰性空间释放。
1)空间预分配
当需要增长字符串时,sds不仅会分配足够的空间用于增长,还会预分配未使用空间。
分配的规则是,如果增长字符串后,新的字符串比1MB小,则额外申请字符串当前所占空间的大小作为free值;如果增长后,字符串长度超过1MB,则额外申请1MB大小。
上述机制,避免了redis字符串增长情况下频繁申请空间的情况。每次字符串增长之前,sds会先检查空间是否足够,如果足够则直接使用预分配的空间,否则按照上述机制申请使用空间。
1 /* 2 * 对 sds 中 buf 的长度进行扩展,确保在函数执行之后, 3 * buf 至少会有 addlen + 1 长度的空余空间 4 * (额外的 1 字节是为 准备的) 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中未使用的空间,因此也不需要担心内存泄漏问题。
4、二进制安全
SDS 的 API 都是二进制安全的: 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。
sds考虑字符串长度,是通过len属性,而不是通过 来判断。
5、兼容部分C语言字符串函数
redis兼容c语言对于字符串末尾采用 进行处理,这样使得其可以复用部分c语言字符串函数的代码,实现代码的精简性。
第3章 链表
列表键的底层之一是链表。(底层也有可能是压缩列表)
当列表键包含了许多元素,或者元素是比较长的字符串的时候,就会用到链表作为列表键的底层实现。
3.1链表和表节点的实现
1、节点结构
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组成双向链表,如下图所示:
2、链表结构
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三个属性为节点值设置类型特定函数,所以链表可以用于保存不同类型的值。
第4章 字典
字典,又称符号表、关联数组、映射,是一种保存键值对的抽象数据结构。
每个键(key)和唯一的值(value)关联,键是独一无二的,通过对键的操作可以对值进行增删改查。
redis中字典应用广泛,对redis数据库的增删改查就是通过字典实现的。即redis数据库的存储,和大部分关系型数据库不同,不采用B+tree进行处理,而是采用hash的方式进行处理。
字典还是hash键的底层实现之一。
当hash键包含了许多元素,或者元素是比较长的字符串的时候,就会用到字典作为hash键的底层实现。
4.1 字典的实现
redis的字典,底层是使用哈希表实现,每个哈希表有多个哈希节点,每个哈希节点保存了一个键值对。
1、哈希表
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的空哈希表结构如下图(左边一列的图)所示:
2、哈希表节点
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是指向另一个哈希表节点的指针,该指针将多个哈希值相同的键值对连接在一起,避免因为哈希值相同导致的冲突。
哈希表节点如下图(左边第一列是哈希表结构,表节点结构从左边第二列开始)所示:
3、字典
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]。
完整的字典结构如下图所示:
4.2 哈希算法
要将新的键值对加到字典,程序要先对键进行哈希算法,算出哈希值和索引值,再根据索引值,把包含新键值对的哈希表节点放到哈希表数组指定的索引上。
redis实现哈希的代码是:
hash =dict->type->hashFunction(key);
index = hash& dict->ht[x].sizemask;
算出来的结果中,index的值是多少,则key会落在table里面的第index个位置(第一个位置index是0)。
其中,redis的hashFunction,采用的是murmurhash2算法,是一种非加密型hash算法,其具有高速的特点。
4.3 键冲突解决
当两个或者以上的键被分配到哈希表数组的同一个索引上,则称这些键发生了冲突。
为了解决此问题,redis采用链地址法。被分配到同一个索引上的多个节点可以用单链表连接起来。
因为没有指向尾节点的指针,所以总是将新节点加在表头的位置。(O(1)时间)
4.4 rehash(重新散列)
随着操作进行,哈希表保存的键值对会增加或减少,为了让哈希表的负载因子(load factor)维持在一个合理范围,当一个哈希表保存的键太多或者太少,需要对哈希表进行扩展或者收缩。扩展或收缩哈希表的过程,就称为rehash。
rehash步骤如下:
1、给字典的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。
2、将保存在ht[0]上面的所有键值对,rehash到ht[1],即对每个键重新采用哈希算法的方式计算哈希值和索引值,再放到相应的ht[1]的表格指定位置。
3、当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自动开始哈希表的收缩工作。
4.5 渐进式rehash
redis对ht[0]扩展或收缩到ht[1]的过程,并不是一次性完成的,而是渐进式、分多次的完成,以避免如果哈希表中存有大量键值对,一次性复制过程中,占用资源较多,会导致redis服务停用的问题。
渐进式rehash过程如下:
1、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两张哈希表。
2、将字典中的rehashidx设置成0,表示正在rehash。rehashidx的值默认是-1,表示没有在rehash。
3、在rehash进行期间,程序处理正常对字典进行增删改查以外,还会顺带将ht[0]哈希表上,rehashidx索引上,所有的键值对数据rehash到ht[1],并且rehashidx的值加1。
4、当某个时间节点,全部的ht[0]都迁移到ht[1]后,rehashidx的值重新设定为-1,表示rehash完成。
渐进式rehash采用分而治之的工作方式,将哈希表的迁移工作所耗费的时间,平摊到增删改查中,避免集中rehash导致的庞大计算量。
在rehash期间,对哈希表的查找、修改、删除,会先在ht[0]进行。
如果ht[0]中没找到相应的内容,则会去ht[1]查找,并进行相关的修改、删除操作。而增加的操作,会直接增加到ht[1]中,目的是让ht[0]只减不增,加快迁移的速度。
4.6 总结
字典在redis中广泛应用,包括数据库和hash数据结构。
每个字典有两个哈希表,一个是正常使用,一个用于rehash期间使用。
当redis计算哈希时,采用的是MurmurHash2哈希算法。
哈希表采用链地址法避免键的冲突,被分配到同一个地址的键会构成一个单向链表。
在rehash对哈希表进行扩展或者收缩过程中,会将所有键值对进行迁移,并且这个迁移是渐进式的迁移。
第5章 跳跃表
跳跃表(skiplist)是一种有序的数据结构,它通过每个节点中维持多个指向其他节点的指针,从而实现快速访问。
跳跃表平均O(logN),最坏O(N),支持顺序遍历查找。
在redis中,有序集合(sortedset)的其中一种实现方式就是跳跃表。
当有序集合的元素较多,或者集合中的元素是比较常的字符串,则会使用跳跃表来实现。
5.1 跳跃表实现
跳跃表是由各个跳跃表节点组成。
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是节点所保存的成员对象。是一个指针,指向一个字符串对象。
表头节点也有后退指针,分值,成员对象,因为不会被用到,所以图中省略。
分值可以相同,成员对象必须唯一。
分值相同时,按照成员对象的字典序从小到大排。
跨度用来计算排位:
第6章 整数集合
整数集合(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的值放入集合。
尽可能地节约内存
降级:
整数集合不支持降级操作
第7章 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩列表有一下几个组成部分:
每个压缩列表节点可以保存一个字节数组或者一个整数值,而每个节点都由previous_entry_length、encoding、content三个部分组成。
previous_entry_length:
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。
因为有了这个长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。
压缩列表的从表尾向表头遍历操作就是使用这一原理实现的。
encoding:
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度
content:
节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
连锁更新:
由于previous_entry_length可能是一个或者五个字节,所有插入和删除操作带来的连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所有连锁更新的最坏复杂度为O(N^2)。
但连锁更新的条件比较苛刻,而且压缩列表中的数据量也不会太多,因此不需要注意性能问题,平均复杂度仍然是O(N)。
第8章 对象
Redis对象系统中包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
实现了基于引用计数的内存回收机制。
8.1 对象的类型与编码
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
8.2 字符串对象
字符串对象的编码可以是int、raw、embstr
如果值是字符串对象,且长度大于32字节,那么编码为raw
如果值是字符串对象,且长度小于等于32字节,那么编码为embstr
embstr的创建只需分配一次内存,而raw为两次,分别创建redisObject结构和sdshdr结构。
相对地,embstr释放内存的次数也由两次变为一次。
embstr的objet和sds放在一起,更好地利用缓存带来的优势。
redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改。
8.3 列表对象
列表对象的编码可以是ziplist或者linkedlist。
当列表对象同时满足下面两个条件时,则使用ziplist:
所有字符串元素的长度都小于64字节
元素数量小于512
ziplist是一种压缩列表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。如下图所示,对象结构中ptr所指向的就是一个ziplist。整个ziplist只需要malloc一次,它们在内存中是一块连续的区域。
linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新malloc一块内存。
8.4 哈希对象
哈希对象的底层实现可以是ziplist或者hashtable。
当列表对象同时满足下面两个条件时,则使用ziplist:
所有键值对的键和值的字符串度都小于64字节
键值对数量小于512
8.5 集合对象
集合对象的编码可以是intset或者hashtable。
满足下面两个条件,使用intset:
所以有元素都是整数值
元素数量不超过512个
8.6 有序集合对象
有序集合的编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。
dict字典为有序集合创建了一个成员到分值的映射。给一用O(1)的时间查到分值。
当有序集合对象同时满足下面两个条件时,则使用ziplist:
所有元素的字符串度都小于64字节
元素数量小于128
第9章 数据库
默认下,Redis客户端的目标数据库为0号数据库。
SELECT 2 可以切换到2号数据库
通过EXPIRE或者PEXPIRE,可以以秒或者毫秒为键设置生存时间。服务器会自动删除生存时间为0的键。
数据库主要由dict和expires两个字典构成,其中dict负责保存键值对,expires负责保存键的过期时间。
第10章 RDB持久化
服务器中的非空数据库以及他们的键值对统称为数据库状态。
RDB持久化可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB可以手动执行,也可以定期执行。可以将某个时间点上的数据库状态保存到RDB文件中。通过该文件也可以还原数据库状态。
RDB文件是一个经过压缩的二进制文件,由多个部分组成。
对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。
10.1 RDB文件的创建和载入
有两个命令可以生成Redis文件,SAVE和BGSAVE。
SAVE会阻塞服务器进程,直到RDB创建完成,期间不能处理任何命令请求。
BGSAVE会派生出一个子进程,由子进程创建RDB,父进程可以继续处理其他命令请求。
BGSAVE执行时,客户端发送的SAVE、BGSAVE这两个命令会被服务器拒绝,BGREWRITEAOF会被延迟到BGSAVE执行完毕后执行。
服务器启动时检测到RDB文件存在,就会自动载入RDB文件。
如果服务器开启了AOF持久化功能,服务器会优先使用AOF文件来还原数据库状态。
10.2 自动间隔保存
用户可以通过save选项设置多个保存条件,只要一个满足,服务器就会执行BGSAVE
save 900 1 ,900秒内对数据库至少进行了1次修改
svae 300 10,300秒内对数据库至少进行了10次修改
第11章 AOF持久化
AOF(append only file)持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。
11.1 AOF持久化的实现
实现可分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
命令追加:
服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
AOF文件的写入与同步:
服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到缓冲区里,所以在每次结束一个事件循环之前,会考虑是否将缓冲区的内容写入到AOF文件里。
11.3 AOF重写
Redis可以创建一个新的AOF文件来替代现有的AOF文件,虽然数据库状态相同,但新的AOF文件不会包含任何浪费空间的冗余命令,新文件体积会小很多。
实现:不是读取现有AOF文件,而是根据现有数据库状态,用最少的命令去得到这个状态。
第17章 集群
17.1 节点
一个Redis集群通常由多个节点组成,刚开始时,每个节点是相互独立的。我们必须将各个独立的节点连接起来。
节点通过握手来将其他节点添加到自己所处的集群中。
127.0.0.1:7000>CLUSTER MEET 127.0.0.1 7001 可以将7001添加到节点7000所在的集群里。
17.2 槽指派
Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个槽(slot)。
数据库中每个键都属于其中一个槽,每个节点可以处理0-16384个槽。
当16384个槽都有节点在处理时,集群处于上线状态,只要有一个槽没有得到处理,那么集群处于下线状态。
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 可以将槽0-5000指派给节点7000负责。
每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。
17.3 在集群中执行命令
客户端向节点发送键命令,节点要计算这个键属于哪个槽。
如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
17.4 重新分片
重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点。
17.5 ASK错误
重新分片期间可能会出现这种情况:属于迁移槽的一部分键值对保存在源节点里,而另一部分保存在目标节点里。
如果节点A正在迁移槽i到节点B,那么当节点A没能在自己数据库中找到命令指定的数据库键时,节点会向客户端返回一个ASK错误,指引客户端到节点B继续查找。
17.6 复制与故障转移
Redis集群的节点分为主节点和从节点。
主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线后,替代主节点。
集群中的每个节点会定期向其他节点发送PING消息,以此来检测对方是否在线。
第18章 发布和订阅
Redis的发布订阅由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。
SUBSCRIBE:客户端可以订阅一个或多个频道,成为这些频道的订阅者。每当有客户端向这些频道发消息的时候,频道的所有订阅者都可以收到这条消息。
PSUBSCRIBE:客户端可以订阅一个或多个模式,成为这些模式的订阅者。每当有客户端向这些频道发消息的时候,订阅频道以及与这个频道相匹配的模式的订阅者都会收到消息。
18.1 频道的订阅与退订
Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里,键是被订阅的频道,值是一个链表,记录了所有订阅这个频道的客户端。
UNSUBSCRIBE用于退订频道。
18.2 模式的订阅与退订
Redis将所有模式的订阅关系都保存在服务器状态的pubsub_patterns链表里。
链表节点中记录了被订阅的模式以及订阅这个模式的客户端。
PUNSUBSCRIBE用于退订模式。
18.3 发送消息
PUBLISH 频道 消息 ,可以将消息发送给频道。
频道以及与频道相匹配的模式的订阅者都会收到消息。
18.4 查看订阅消息
PUBSUB NUMSUB 【channel-1 ... channel-n】 接受任意多个频道作为输入参数,返回这些频道的订阅者数量。
PUBSUB NUMPAT ,返回服务器当前被订阅模式的数量。
第19章 事务
Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。
事务提供了一种将多个命令请求打包,然后一次性、按顺序执行多个命令的机制。
事务在执行期间,服务器不会中断事务去执行其他命令。
事务首先以MULTI开始,接着多个命令放入事务中,最后由EXEC命令将这个事务提交。
19.1 事务的实现
MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态。
切换到事务状态后,如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI,那么服务器会立即执行,其他命令则会放入事务队列里。
处于事务状态时,EXEC会被立即执行。服务器会遍历事务队列,执行队列中的所有命令,最后将结果返回给客户端。
19.2 WATCH命令的实现
WATCH命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC执行时,检查被监视的键是否至少有一个被修改过了,如果是,那么服务器将拒绝执行事务。
每个Redis数据库都保存着一个watched_keys字典,这个字典的键是被WATCH监视的键,值是一个链表,记录了所有监视相应数据库键的客户端。
如果某个键被修改,那么监视该键的客户端的REDIS_DIRTY_CAS标识就会被打开。
执行EXEC时,服务器会根据客户端的REDIS_DIRTY_CAS标识是否被打开来决定是否执行事务。
19.3 事务的ACID特性
Redis的事务和传统的关系型事务最大的区别在于,Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现错误,整个事务也会继续执行下去。
如果一个事务在入队命令过程中,出现了命令不存在或者命令格式错误,那么Redis将拒绝执行这个事务。
事务执行过程中,出错的命令不会对数据库做任何修改。
只有当服务器运行在AOF持久化模式下,并且appendfsync为always时,这种配置的事务才具有持久性。