第 2 章 简单动态字符串(SDS)
1. Redis使用自己构建一种名为简单动态字符串(simple dynamic string ,SDS)的抽象类型,作为Redis的默认字符串。
2. SDS的结构:
free属性:表示这个SDS没有分配任何使用的空间;
len属性:表示SDS保存字符串的长度;
buf[]属性:用于保存字符串(数组的最后一个字节则保存了空字符'\0',且这空字符不计算SDS的len属性里面)。
3. SDS的好处:
降低获取字符串长度的复杂度(0(n)-->o(1)),确保获取字符串长度工作不会成为Redis的性能瓶颈。
杜绝缓冲区溢出,例如SDS在使用sdscat函数(拼接操作)时,首先后检查SDS的空间是否足够,不够先扩展空间,然后在拼接。
减少修改字符串时带来的内存重分片次数,通过未使用空间(即free属性记录的长度),SDS实现了空间预分配和惰性空间释放两种优化策略。
4. 空间预分配公式:
SDS长度(len属性的值,单位:字节)小于1MB时,程序分配len属性同样大小的free属性的未使用空间。
SDS长度大于1MB时,程序分配1MB的未使用空间。
5. 惰性空间释放:
当SDS缩短保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而使用free属性将这些字节的数量记录起来,并且等待将来使用。
6. Redis二进制安全:
SDS的API都是二进制安全的(binary-safe),所有SDS API都会以二进制的方式处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或假设,数据写入时是什么样的,取读还是什么样的。
7. 兼容部分C语言字符串函数,实现复用。
8. C字符串与SDS之间的区别:
9. SDS API:
第 3 章 链表
1. 链表节点结构:
2. 链表结构:
3. Redis的链表实现的特性总结:
- 双端:链表节点带有prev和next指针,获取某个节点的 前置节点和后置节点的复杂度都是O(1)
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
- 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
- 带链表长度计数器:list结构的len属性来对list持有的链表节点进行计数,程序获取链表节点数量的复杂度为O(1)
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性来为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值
4. 链表和链表节点的API:
见书籍P21
5. 链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。
第 4 章 字典
1. 概念:
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key - value pair)的抽象数据结构;
并且字典中每一个键都是独一无二的;
Redis的数据库就是使用字典来作为底层实现的,对数据库的增删查改操作是构建在对字典的操作之上的;
字典还是哈希键的底层实现之一。
2. 字典的实现:
字典使用哈希表作为底层实现,一个哈希表里面有多个哈希节点,每个哈希节点就保存了字典中的一个键值对。
3. 哈希表结构:
table属性是一个数组,数组中的每一个元素都是指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
4. 哈希表节点结构:
next属性指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接在一起,以此解决键冲突的问题。
5. 字典结构:
type属性和 privata属性是针对不同类型的键值对,为创建多态字典而设置的。
type属性:是一个指向dictType结构的指针,dictType结构中保存了用于操作特定类型键值对的函数, Redis会为不同的字典设置不同的类型特定函数。
privata属性:保存了类型特定函数的可选参数。
ht属性:包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
dictType结构:
字典数据结构:
6. Redis的哈希表解决键值冲突方法:
使用链地址法(就是散列表)来解决键值冲突,每个哈希节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用单向链表链接起来,这就解决了键冲突的问题。
7. 拓展和收缩哈希表的工作可以通过执行rehash操作重新完成,简单来说:对ht[0]所有的键值对进行重新计算哈希值和索引值,再把它们放入ht[1]中。
8. 当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
- 其中哈希表的负载因子可以通过公式:
负載因子=哈希表已保存节点数量/哈希表大小
load factor =ht[0].. used/ ht[0]. size
9. 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
10. 渐进式rehash:
为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次,渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。简单来说:每次只处理ht[0]哈希表中,一个索引值上所有的键值对。每次处理的时机是在对字典的添加或删除或查找或更新操作时,附带执行。
11. 渐进式rehash执行期间:
- 查询操作:程序先在ht[0]里面进行查找,如果没有再继续到ht[1]里面进行查找。
- 添加操作:直接添加到ht[1]中。
12. 字典API:
见书籍P36
第 5 章 跳跃表
11. 概念:
- 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
- 跳跃表支持平局O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
- Redis使用跳跃表的两个地方:有序集合和集群节点中用作内部数据结构。
2. 跳跃表数据结构:
Redis跳跃表由zskiplistNode(用于表示跳跃表节点)和zskiplist(用于保存跳跃表节点的相关信息)两个结构定义。
- zskiplistNode:标记节点的各个层。后退指针(BW):当前节点的前一个节点,后退指针在程序从表尾向表头遍历时使用;分值(score):节点所保存的分值,在跳跃表中,节点按各自所保存的分值从大到小排列;成员对象(obj):节点保存的成员对象。
- zskipList:header:指向跳跃表的表头节点;tail:指向跳跃表的表尾节点;level:记录目前跳跃表内,最大层数节点的层数(表头节点的层数不计算在内);length:记录跳跃表的长度,即,跳跃表目前包含的节点数据量。
3. 其他:
- 每个跳跃表节点层高都是1至32之间的随机数。
- 在同一个跳跃表中,多个节点可以包含相同的分值,但是每个节点的对象必须是唯一的。
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
第 6 章 整数集合
1. 概念:
- 整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值的元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
- 它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
2. 数据结构:
3. 升级(upgrade):
- 概念:新增一个元素到整数集合里面,并且新元素的类型比现有整数集合所有元素的类型都要长时,整数集合需要先升级,然后能将新元素添加到整数集合里面。
- 步骤:1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;2)将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位置上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变;3)将新元素添加到底层数组里面。
- 好处:提升灵活、节约空间。
3. 降级:整数集合不支持降级操作,一旦对数组升级,编码就会一直保持升级后的状态。