文章目录
首先需要明确, redis有五种基本类型的对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
简单动态字符串SDS
SDS时redis的默认字符串表示,除了被用来保存数据库的字符串值之外,还被用于缓冲区:AOF缓冲区和客户端状态的输入缓冲区。Redis只会使用C字符串作为字面量。
基本结构
struct sdsdhr{
int len; // 已经使用的字节长度
int free; // 未使用的字节数量
char buf[]; // 字节数组,实际用来保存字节。最后一个字节保存空字符,但是这个位置不计算为长度。
}
设计优点
- 可以常数复杂度获得字符串长度,
STRLEN
复杂度为 O ( 1 ) O(1) O(1);杜绝缓存区溢出,优先比较拼接的字符串和当前的剩余空间的大小。 - 采用空间预分配和惰性空间释放两个方法优化内存分配。
- 空间预分配:小于1MB时候,再需要分配空间时候,会额外分配空间,使得len=free;大于1MB会额外给1MB空间。
- 惰性空间释放:程序并无主动收回空余的空间。
- 二进制安全,不使用/0作为结束标志,而是len;兼容C字符串函数
链表
应用广泛,列表键的底层实现之一就是链表。还有发布与订阅,慢查询等都是链表。
基本结构
// quicklist节点定义 --quicklist.h
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前驱指针 */
struct quicklistNode *next; /* 后驱指针 */
unsigned char *zl; /* ziplist */
unsigned int sz; /* ziplist的bytes大小*/
unsigned int count : 16; /* ziplist中的元素数量 */
unsigned int encoding : 2; /* 是否进行LZF压缩 RAW==1 or LZF==2 */
unsigned int container : 2; /* 是否包含ziplist NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* 是否曾被压缩 */
unsigned int attempted_compress : 1; /* 测试使用字段 */
unsigned int extra : 10; /* 预留内存空间 */
} quicklistNode;
// quicklist定义 --quicklist.h
typedef struct quicklist {
quicklistNode *head; /* 头节点指针 */
quicklistNode *tail; /* 尾节点指针 */
unsigned long count; /* 元素总数(所有ziplist中的所有元素) */
unsigned long len; /* quicklist节点数 */
int fill : 16; /* 节点的填充因子 */
unsigned int compress : 16; /* LZF算法的压缩深度; 0=off */
} quicklist;
链表的特点
双端,无环,含有表头表尾指针,还带有长度计数器和多态性质(可以保存不同类型的值)。
字典
基本结构
table属性是一个数组,数组中每个元素都是一个指向dictEntry结构的执政,每个dictEntry结构保存着一个键值对。
字典包括两个哈希表,一般字典只使用ht[0]哈希表,只有在rehash时候才会使用ht[1]。rehash记录了当前的进度,如果没有进行重新哈希,为-1。hash算法用的MurmurHash2,得到的哈希值再与mask与得到index。
解决哈希冲突用拉链法,并且新的节点总是被加在链表头 O ( 1 ) O(1) O(1)
// 哈希表dict具体结构定义 --dict.h
typedef struct dictht {
dictEntry **table; /* 二维结构 数组+链表 ,表明了拉链法解决hash冲突*/
unsigned long size; /* table[]大小 */
unsigned long sizemask; /* table[]大小的掩码 size-1(用以计算索引值)总是等于size-1 */
unsigned long used; /* 已有节点个数 */
} dictht;
// dict中的节点定义 --dict.h
typedef struct dictEntry {
void *key; /* 节点的键 */
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; /* 节点的值 */
struct dictEntry *next; /* 指针链接到下一个节点 */
} dictEntry;
// dict定义 --dict.h
typedef struct dict {
dictType *type; /* 保存私有方法的对象指针 */
void *privdata; /* 私有数据 */
dictht ht[2]; /* 每个dict包含两个dictht,用来rehash */
long rehashidx; /* 执行rehash的索引,没有rehash时为-1 */
unsigned long iterators; /* 运行时的迭代器数 */
} dict;
rehash的操作
每次扩展空间都是变为ht[0].used*2之后的第一个2次幂,减少是大于变为ht[0].used的2次幂。
当服务器没有执行BGSAVE时候,负载因子大于等于1开始扩容,否则大于5。负载因子定义为:.used/.size; 缩小的负载因子是0.1。
在扩展或者收缩时,采用的渐进式哈希,避免了rehash 的巨大计算量,具体如下:
- 为ht[1]开辟空间,rehashidx赋值0,开始从*table[0]开始将ht[0]的数据rehash至ht[1]
- 随着rehashidx自增,对table[rehashidx]进行rehash。在rehash期间,对disc的写操作会同时作用于ht[0]和ht[1]
- ht[0]的数据全部rehash至ht[1]时,rehash完成。rehashidx赋值-1。
- 将ht[1]赋值给ht[0],清空ht[1]。
跳表
支持平均 O ( l o g n ) O(logn) O(logn),最坏 O ( N ) O(N) O(N)复杂度的节点查找。是有序集合键的底层实现之一。每个节点的层高是1-32之间的随机数。
基本结构
由两个数据结构组成,类似链表。在同一个跳表中,每个节点保存的成员对象必须是唯一,但是分数可以不必,相同分数按照成员字典序排序。
// skiplist定义 --server.h
typedef struct zskiplist {
struct zskiplistNode *header, *tail; /* skiplist的头、尾节点 */
unsigned long length; /* 跳跃表长度/节点数(不包括头节点)、也就是元素数量 */
int level; /* 目前跳表内毒,层数最大的节点的层数,表头不算 */
} zskiplist;
// skiplist节点定义 --server.h
typedef struct zskiplistNode {
robj *obj; /* 成员对象 */
double score; /* 节点的分数,所有节点按照分数排序 */
struct zskiplistNode *backward; /* 后退指针 */
struct zskiplistLevel {
struct zskiplistNode *forward; /* 链接层与层的前驱指针 */
unsigned long span; /* 路径跨度,搜索元素时累加span可得到rank排名 */
} level[]; /* skiplist的层 ,每层具有前进指针和跨度两个属性*/
} zskiplistNode;
结构特点
可以进行快速的查询,并且因为是排序的,可以按照分值范围进行查询等操作。
整数集合
是集合键的底层实现之一,当一个集合只包含整数值元素,并且在几个元素数量不多时,redis采用整数集合作为集合键的底层实现。
基本结构
保存的时不会重复的元素,并且可以不同的大小。contents是底层实现,各项在数组中按照大小顺序从小到大有序排列。每个节点可以保存一个节点数组或者整数值。
typedef struct intset{
uint32_t encoding; // 编码方式int16_t,int32_t,int64_t
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
}intest;
结构特点
- 升级与降价
当新加入的元素比数组元素类型长,整个整数集合要进行升级。数组进行扩容,所有元素都转化为与新元素相同的类型,保证数组的有序性不发生改变。因此向整数集合添加新元素的时间复杂度为 O ( N ) O(N) O(N)。需要注意的是,整数集合不支持降级操作。 - 优点:提升了灵活性,不用担心类型错误;节约内存,在有需要的时候才进行升级。
压缩列表 ziplist
压缩列表是列表键和哈希键的底层实现之一。是一种节约内存的顺序型数据结构。
基本结构
包含压缩列表和压缩节点两种。
一个压缩列表可以包含任意多个节点,每个节点保存一个字节数组或者一个整数值。
- zlbytes:记录整个列表占用的内存字节数。
- zltail:记录表尾距离表的起始地址的偏移地址。可以 O ( 1 ) O(1) O(1)复杂度得到表尾地址。
- zllen:记录了表的节点数量,小于65535才可,不然需要遍历。
- entry:列表的节点,节点的长度不一定。
- zlend:特殊值0xFF表示末尾。
- previous_entry_length:前一节点的长度。前一节点长度小于254字节,当前pel长1字节,否则5字节且第一字节是0xFE。可以根据当前的节点的起始位置得到前一节点的起始地址。从而从尾向头进行遍历。
- encoding:表示了编码方式。
- content:表示具体的内容。
结构特点
- 连锁更新问题
某个节点一旦需要重新分配地址,如长度变化等,就需要更新后续的全部previous_entry_length
对象
利用五种不同类型的对象,redis可以在执行命令之前根据对象的类型进行判断命令是否合法。另外一个好处是可以针对不同的适用场景采用合适的数据结构进行实现。
并且redis实现了基于引用计数的内存回收机制,某个对象不被引用就会被回收;并且可以实现对象共享,通过让多个数据库键共享一个对象来节约内存。
对象的类型和编码
在redis中新建一个键值对,会创建一个字符串值得键对象和一个值对象。其中值对象可以是五种基本类型。并且会有一个指向底层实现数据结构的指针。
字符串对象
字符串对象的编码可以是int、raw或者embstr。
- 如果字符串对象保存的是整数值,并且可以用long来表示。那么会以整数值进行保存,编码类型为int。
- 如果是字符串值,大于39字节是SDS的类型,编码类型为raw;小于39为embstr编码。embstr编码可以降低内存分配次数。
列表对象
可以使用ziplist或者linkedlist
ziplist编码中,每个节点都存了一个列表元素;linkedlist这个双段列表中,每个节点都保存了一个字符串对象,每个字符串对象保存了一个列表元素。
- ziplist编码:当列表对象中所有的字符串元素长度小于64字节并且元素数量小于512个。
- linkedlist编码:其余情况。
哈希对象
可以使用ziplist或者hashtable。
ziplist中,先把保存了键的压缩链表节点压在队尾,再把保存了值的压缩列表节点压入队尾。hashtable使用了字典。
- ziplist编码:所有键值对字符串长度小于64字节并且数量小于512。
- hashtable编码:其余的情况。
集合对象
可以是intset或者hashtable编码。当集合所有元素是整数且数量不超过512个时采用intset编码,否则是hashtable编码。
有序集合对象
可以是ziplist或者skiplist编码。当数量小于128且元素长度小于64字节使用ziplist。
对于skiplist编码,使用了zset作为底层结构,一个zset结构同时包含了一个字典和一个跳跃表。其中跳跃表按照分值从小到大保存了全部的元素,因此可以对有序集合进行范围查找等。此外zset还用字典等建立一个从成员到分值的映射,可以再 O ( 1 ) O(1) O(1)的复杂度下找到成员。并且两种数据结构会通过指针来共享相同元素的成员和分值。
对象的特点
类型检查与命令多态
在执行一个特定的指令之前,会检查数据库键对应的值对象的类型是否可以合法。某些质量可以对于多个类型的多态,某些时基于编码实现的多态。
内存回收
每个对象都有一个引用计数,在被初始化时候为1,每被一个新程序引用便加一,不在被引用就减一,引用为0时该对象占用的内存被释放。
对象共享
可以让数据库键的值指向一个现有的值对象(一般是0-9999的字符串对象),并将被共享的对象的引用计数加一。但是一般都是针对字符串键或者嵌套了字符串的键,因为比较容易在 O ( 1 ) O(1) O(1)的复杂度内比较相等。
LRU属性
会记录对象最后一次被程序访问的时间,在内存满了之后lru大的那部分键会被优先释放。