《Redis设计与实现》读书笔记 第一部分
第一部分从第二章到第八章,主要解决下面几个问题:
redis的底层是由哪些数据结构实现的,它们的应用场景又是什么redis的五大对象分别用了哪些数据结构实现redis的内存回收机制
SDS简单动态字符串
struct sdshdr {
int len;
int free;
char buf[];
}

- 因为
len的存在,获取字符串长度的复杂度为O(1)。 - 同时利用
free,实现了空间预分配和惰性空间释放两种优化策略。- 空间预分配:修改之后
SDS长度小于1MB,将分配与len同样大小的未使用空间,此时len和free属性相同;否则,将会分配1MB的未使用空间。空间预分配可以减少连续执行字符串增长所需的内存重分配次数,也可以杜绝缓冲区溢出。 - 惰性空间释放:当需要缩短
SDS保存的字符串时,不立即使用内存分配回收多余字节,而是使用free属性将字节数量记录起来,避免缩短字符串时所需的内存分配,为将来可能的增长操作提供优化。
- 空间预分配:修改之后
SDS的API是二进制安全的,程序不会对其中的数据做任何限制、过滤或者假设。SDS使用len属性的值而不是空字符来判断字符串是否结束。
链表
除了列表键,发布订阅/慢查询/监视器/服务器保存客户端信息/客户端输出缓冲区,都使用链表作为底层实现。
struct list{
listNode * head;
listNode * tail;
unsigned long len;
...
}
listNode是一个双链表,包含了prev和next指针,可以保存不同类型的值。

优点:
- 获取表头和表尾节点的复杂度为
o(1) - 获取链表中节点数量的复杂度为
o(1)
字典
由于C中没有字典,因此redis实现了自己的字典。它可以用来表示数据库,也是哈希键的底层实现之一。底层使用哈希表作为实现,一个哈希表由多个哈希表节点组成,每个哈希表节点保存字典中的键值对。
哈希表的结构:
struct dictht{
// 每个dicEntry保存一个键值对
dicEntry **table;
// 大小/掩码
unsigned long size;
unsigned long sizemask;
unsigned long used;
}
dicEntry的结构:
struct dictEntry{
void *key;
union {
void *val;
unit64_tu64;
int64_ts64;
}
// 指向另一个哈希表节点的指针,用来解决冲突
struct dictEntry *next;
}
由于没有指向表尾的指针,因此每当出现了冲突,都会将新节点添加到链表的表头位置。
字典的结构:
struct dict{
...
// 通常使用ht[0],ht[1]在rehash时使用
dictht ht[2];
// 没有rehash时,值为-1
int rehashidx;
}
结构图如下:

当哈希表保存的键值对太多或者太少,需要进行相应的扩展或者收缩,即进行rehash,步骤如下:
- 为
ht[1]分配空间,大小取决于ht[0] - 将
ht[0]中的所有键值对rehash到ht[1]中,采用渐进式rehash - 当
ht[0]包含的所有ht[1]之后,释放ht[0],将ht[1]设置为ht[0]
当服务器1)没有执行
BGSAVE或者BGREWRITEAOF且负载因子(used/size)大于1;2)执行BGSAVE或者BGREWRITEAOF且负载因子大于5时会自动进行扩展;
当服务器负载因子小于0.1时,会自动进行收缩。
渐进式rehash需要注意的是:
- 每次进行
rehash工作完成后,rehashidx属性值增一; - 当
rehashidx属性值为-1,表示rehash完成; - 字典的增删改查会在两个哈希表上进行,比如查找键时,会首先在
ht[0]中查找;新添加键一律保存到ht[1]中。
跳跃表
通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的,支持平均O(logN),最坏O(N)的复杂度进行查找,实现比平衡树更为简单。
redis中它只用于实现有序集合键,以及集群节点中用作内部结构。
zskiplist的结构定义如下:
struct zskiplist{
struct zskiplistNode * header, * tail;
// 跳跃表的长度,可以在O(1)时间内返回
unsigned long length;
// 跳跃表的最大层高,表头节点并不包含在内
int level;
}
它包括指向zskiplistNode的表头表尾指针:
struct zskiplistNode{
// 每个层都带有两个属性:前进指针和跨度
struct zskiplistLevel{
// 访问表尾方向的其他节点,一次可以跳过多个节点
stuct zskiplistNode *forward;
// 记录当前前进指针与当前节点之间的距离
unsigned int span;
}level[];
// 用于从表尾向表头遍历时使用,每次只能后退一个节点
struct zskiplistNode *backward;
// 节点保存的分值,从小到大排列
double score;
// 节点中所保存的成员对象
robj * obj;
}
需要注意的是,每创建一个新的节点时,都会随机生成一个介于
1到32之间的值作为level数组的大小。
跨度实际上是用来计算排位的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累计,就是目标节点的排位。

整数集合
intset是redis用于保存整数值的集合抽象数据结构,可以保存int16_t、int32_t或者int64_t的整数值,并且不会重复,结构如下:
struct intset{
unit32_encoding;
uint32_t length;
// 底层实现,每个元素都是contents数组的数组项
int8_t contents[];
}
虽然
contents声明为int8_t类型的数组,但是它真正保存的值取决于encoding的值。

当需要将新元素添加到现有的整数集合里,并且新元素的类型比现有元素的类型都要长时,整数集合需要先进行升级:
- 根据新元素类型,扩展底层数组空间大小
- 将原有元素的类型转换成为新元素类型,并且从后往前分配到新位置处,最后将新添加的元素放在末尾或者开头(因此添加一个新元素的时间复杂度为
O(N)) - 修改
encoding
由此可见,升级可以提升灵活性和节约内存。整数集合不支持降级,一旦升级,将永远保持。
压缩列表
ziplist可以用于实现列表键和哈希键,由一系列特殊编码的连续内存块组成的顺序性结构。它可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值:
struct ziplist{
uint32_t zlbytes;
uint32_t zltail;
unit16_t zllen;
unit8_t zlend;
若干节点,可以为字节数组或者整数值;
}
每个节点都由previous_entry_length(前一个节点的长度,因而可以根据当前节点的起始地址计算前一个节点的起始地址,长度可以是1字节或者5字节)、encoding(content所保存数据的类型及长度,例如001011表示保存的是一个长度为11的字节数组)、content三个部分组成。

由于
previous_entry_length的长度是记录前一个节点的长度,因此添加或者删除节点都可能导致连锁更新,最坏复杂度为O(N^2),只是这种情况比较少出现。
对象
redis包含了5种类型的对象:字符串对象,列表对象,哈希对象,集合对象,有序集合对象。每一种都用到了至少一种基本数据结构。
对象的基本数据结构:
struct redisObject{
unsigned type:4;
// 通过encoding设定对象使用编码,可以提升灵活性和效率
unsigned encoding:4;
void *ptr;
// 引用计数
int refcount;
// 对象最后一次被访问的时间,用以计算对象的空转时间
unsigned lru:22;
}
type可以是REDIS_STRINT,REDIS_LIST,REDIS_HASH,REDIS_SET,REDIS_ZSET的一种。对于redis来说,键总是字符串对象,而值可以使上面五种对象中的一种。
字符串对象
字符串对象是唯一一种会被其他四种类型对象嵌套的对象。
编码可以为:
int:整数值,且可以使用long类型表示;
但是
long double类型的浮点数仍然是使用字符串表示的
raw:字符串值,且长度大于32字节,使用SDS保存;
embstr:字符串,且长度小于32字节。相比raw,虽然也使用sds结构,但是它只需要调用一次内存(raw需要调用两次)分配函数分配空间,空间中依次包含redisObject和sdshdr。
列表对象
编码可以为:
ziplist: 所有字符串元素长度小于64字节,且个数小于512
linkedlist: 不能用ziplist保存的。
哈希对象
编码可以为:
ziplist: 所有键和值字符串元素长度小于64字节,且个数小于512
采用压缩列表保存时,每当有新的键值对需要加入,总是现将键的压缩列表节点推入,再保存值的压缩列表节点。因此,保存同一键值对的节点总是紧挨在一起,并且键值对之间的顺序根据时间先后排列。
hashtable: 不能使用ziplist保存的。
集合对象
编码可以为:
intset: 所有保存的对象都是整数值,且个数小于512
hashtable: 不能使用intset保存的。字典中的每一个键都是字符串对象,字典的值全部设置为NULL。
有序集合对象
编码可以为:
ziplist: 每个元素成员的长度都小于64字节,且元素数量小于128个
每个集合元素使用两个紧挨一起的节点保存,第一个节点保存成员,第二个节点保存分数,集合元素按照分值从小到大进行排序
skiplist: 不能使用ziplist保存的。
使用包含了一个字典和一个跳跃表的
zset作为底层实现:
struct zset{
// 每个跳跃表节点的object都保存了元素的成员,score属性保存元素的分值,可以实现`zrank`/`zrange`等命令
zskiplist *zsl;
// 为有序集合创建了从成员到分值的映射:键保存了成员,值保存了分值,可以实现O(1)复杂度查找给定成员的分值
dict * dict;
}
zsl和dict会通过指针共享相同元素的成员和分值,为了能让有序集合的查找和范围型操作都可以尽快执行,需要这两种数据结构实现有序集合。
类型检查和命令多态
类型检查主要是通过redisObject结构中的type属性实现。而命令多态主要根据redisObject中的encoding来实现,它会调用指定数据结构的API。
内存回收和对象共享
内存回收主要依靠引用计数实现,同时refcount还可以用于实现对象共享,节约内存。
一般情况下,redis会在初始化服务器时,创建0-9999的所有整数值作为共享对象(受到cpu的时间限制,只对整数值的字符串对象进行共享)。

浙公网安备 33010602011771号