第二章 SDS(简单动态字符串)
redis只会用C字符串作为字面量,而大部分字符串表示用SDS。
struct sdshdr{ int len;//基于已使用的字节长度,即SDS的长度 int free;//记录未使用的字节数量 char buf[];//保存字符串 }
SDS的优点:
1.可以以O(1)复杂度获取字符串的长度(只用访问len即可),而C字符串需要O(N)(N为字符串字节长度)。
2.可杜绝缓冲区溢出。SDS API如果要对SDS扩展长度,API会先通过查看free检查是否又足够多的内存满足修改,若不够会自动扩展到所需要求,并采取空间预分配措施。
3.减少内存重分配次数。主要有两种措施:A.空间预分配。对SDS进行增长操作时,若修改后SDS长度少于1MB,那么分配与修改后len相等大小的未使用空间。若大于1MB,则只分配1MB未分配空间。B.惰性空间释放。缩短SDS时不释放因此多出来的未分配空间,而是保存在SDS当中,并用free记录下来。
4.二进制安全。SDS可以保存带有多个空字符的字符串,所以可以用来保存二进制数据。
5.兼容部分C字符串函数。SDS的buf一样遵循C字符串以空字符结尾的惯例,所以可以重用部分
第三章 链表
列表键包含数量比较多的元素或者都是比较长的字符串时底层会用到链表。
每个链表结点用listNode表示
typedef struct listNode{ //前置节点 struct listNode *prev; //后置节点 struct listNode *next; // 节点的值 void *value; }listNode;
用list持有链表
typedef struct list{ //链表头节点 listNode *head; //链表尾节点 listNode *tail; //链表长度 unsigned long len; //节点值复制函数 void *(*dup)(void *ptr); //节点值释放函数 void (*free)(void *ptr); //节点值对比函数 int (*match)(void *ptr,void *key); }list;
链表双端,无环,可保存各种不同类型的值。
第四章 字典
redis中,字典作为数据库和部分哈希键的底层实现,字典的底层实现为哈希表。
哈希表由dictht结构定义:
typedef struct dictht{ dictEntry **table;//哈希表数组 unsigned long size;//哈希表表大小 unsigned long sizemask;//表大小的掩码,用于哈希函数计算索引值 unsigned long used;//已经存储的节点数量 }dictht
哈希表节点由dictEntry定义
typedef struct dictEntry{ void * key; //key union{ void *val; uint64_t u64; int64_t s64; } v; //valie struct dictEntry *next;//指向下一个哈希表结点,组织成链表 }dictEntry
字典由dict定义:
typedefr struct dict{ //类型特定函数 //指向一个dictType结构的指针,dictType保存着操作键值对的各种函数 dictType *type; //私有函数 //为dictType中的函数传参数 void *privdata //哈希表 //一般只使用ht[0],ht[1]进行rehash操作 dictht ht[2]; //rehash索引 //记录rehash进度,rehash不在进行时值为-1 int trehashidx; }dict;
下图为普通状态的字典.
当字典作为数据库和部分哈希键的底层实现使用MurmurHash2算法计算哈希值(根据哈希值&sizemask得到索引值,根据索引值把键值对放进指定索引)
哈希表使用链地址法解决键冲突(将分配在同一索引值的哈希表节点用next指针构成单向链表)
rehash(为了将哈希表的负载因子维持在一个合理范围而对哈希表大小进行扩展或者收缩的操作)(负载因子=哈希表已保存节点数量/哈希表大小):
1.如果是扩展大小,则ht[1]为大于当前存储节点数量两倍最近的一个2的幂数,如果是收缩大小的话就是当前存储节点数最近的那个幂数。
2.将ht[0]的键值对rehash到ht[1]中,释放ht[0]
3.ht[1]设置为ht[0],并为ht[1],新建一个空白哈希表。
渐近式rehash(进行此操作时字典操作在两个哈希表进行):
将rehashidx设置为0后,根据rehashidx值从ht[0]的第一个索引一个一个rewqhash.
第五章 跳跃表
跳表是一种有序的数据结构,通过在每个节点中维持多个指向别的节点的指针来打到快速访问的目的。
跳表支持平均O(logn),最坏O(n)的复杂度进行查找,也能顺序批量处理结点,效率可一个平衡树媲美,但是实现比平衡树简单。
redis中的应用:有序集合键,集群节点中用作内部数据结构
跳跃表节点有zskiplistNode结构定义
typedef struct zskiplistNode { // 成员对象:字符串对象(其保存一个SDS),唯一 robj *obj; // 分值:可以不唯一,分值较小的节点靠近表头,分值相同时成员对象字典序较小的靠近表头 double score; // 后退指针:每次只能后退一个节点 struct zskiplistNode *backward; genjue // 层:层高为1-32之间的随机数 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度---前进指针所指向节点与当前节点的距离,用来计算目标节点的排位//NULL跨度为0 unsigned int span; } level[]; } zskiplistNode;
跳跃表信息由zskiplist结构定义:
typedef struct zskiplist { // 表头节点和表尾节点 struct zskiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
第六章 整数集合
用于保存整数值的集合数据结构。集合键的底层实现之一(当集合键都是数值,并且集合元素数量 不多时,才采用)。
整数集合由insert结构定义:
typedef struct intset{ //编码方式:决定元素16,32,64位 uint32_t encoding; //集合元素数量 uint32_t length; 保存元素的数组 int8_t contents[]; }
contents:contents中的项按照值大小从小到大有序排列,数组中不包含任何重复项。
length:contents的长度。
encoding:=INTSET_ENC_INT16:contents为int16_t数组,数组中的每个项目也是int16_t类型,取值范围:-32768~32767
=INTSET_ENC_INT32:contents为int32_t数组,数组中的每个项目也是int32_t类型,取值范围:-2147483648~2147483647
=INTSET_ENC_INT64:contents为int64_t数组,数组中的每个项目也是int64_t类型,取值范围:-9223372036854775808~9223372036854775807
升级
当新加入的元素值比现有类型要长时,集合要先进行upgrade。步骤为
1.根据新元素类型,扩展整数集合底层数组空间大小,并未新元素分配空间 //16*4->32*4
2.将数据组中原有数值转换为与新元素相同的数据类型,并将转换后的数值放置到正确位置上
3.将新元素加入到新数组中
只可升级,不可降级。
第七章 压缩队列
列表键和哈希键底层实现之一。当列表键值包含少量列表项目,并且要么每个列表项都是小整数值,要不是长度比较短的字符串,redis就使用压缩列表做列表键的底层实现。
是为节约内存而开发。
由一系列特殊编码的连续内存快组成的顺序型数据结构
压缩列表结构如下
压缩列表节点结构如下
encoding:记录了content属性保存的数据的类型以及长度
content:负责保存节点的值
考虑一种情况,在一个压缩列表中,有多个连续的、长度介于250~253字节之间的节点,因为所有节点的长度都小于254,所以previous_entry_length属性都是一个字节长。如果在压缩表头加入一个长度大于254的新节点,后续节点的previous_entry_length都需要变为5个字节长度,从而引发连锁的结构变更。
实际应用中,这种情况较少。
第八章 对象
SDS、链表、字典、压缩列表、整数集合是Redis中的主要数据结构,但是Redis并没有直接使用这些数据结构实现键值对数据库。而是通过这些结构创建了一个对象系统,这些对象包括字符串对象、列表对象、哈希对象、集合对象、有序集合对象五类对象。
Redis使用对象表示数据库中的键与值。每当我们在数据库中创建一个键值对时,都会创建两个对象:键对象、值对象,每个对象都有redisObject结构表示。
说明:redisObject还包含了refcount、lru属性,分别用于计算引用次数、对空时长(就是一个对象被闲置的时间)
type:对象类型
encoding:记录了对象的使用数据结构
通过encoding,在不同场合为一个对象设置不同的编码,可以优化对象在某一场景下的效率。
服务器执行命令前,会先检查type类型来看能否执行给到命令。
2.字符串对象
字符串对象的编码可以是int、embstr、raw。
如果字符串的长度<=32,字符串对象使用 embstr编码存储。 embstr比 raw性能更高。
差异化:embstr需要一次构造、一次释放,raw不是,embstr数据存在同一块内存,更能利用缓存优势。
3.列表对象
列表对象的编码可以是:ziplist(元素数量<512且字符串长度全部<64字节)或linkedlist.
4.哈希对象
哈希对象的编码可以是ziplist(字符串长度<64字节&数量<512个)或hashtable
5.集合对象
集合对象的编码可以是:intset(元素全为整数&数量<512个)或hashtable
6.有序集合对象
有序集合对象的编码可以是ziplist(元素长度<64字节&数量<128个)、skiplist(字典+跳跃表)
7.refcount count(引用计数)作用:
1.实行内存回收机制。新对象的refcount默认=1,当它被一个新程序使用refcount+1,不再被一个程序使用-1,为0时对象所占用内存被释放
2.对象共享.当拥有某个值的对象被创建时,当某个程序需要使用一个相同值得对象,无需创建新对象而更享此对象即可,然后将此对象refcount+1。redis初始化时默认创建1w个包含从0-9999的字符串对象。(不能共享字符串值的字符串对象).
8.空转时长
通过lru记录对象最后一次被程序访问的时间,内存回收时lru值较大的对象优先被回收。