Redis读书笔记(一)

2023-06-13,

Redis数据结构

1 简单动态字符串

Simple dynamic string 的实现

// sds.h/sdshdr
struct sdshdr {
int len; //记录buf数组中已使用的字节数, 不包括结尾空字符'\0' int free; //记录buf数组中未使用的字节数 char buf[]; //字节数组, 保存字符串
};

简单动态字符串SDS与C字符串的区别

获取字符串长度的时间复杂度

C字符串: O(N), 需要遍历字符串
SDS: O(1)
缓存区溢出问题
C字符串修改时需要为目标字符串分配足够的内存,否则会产生缓存区溢出
SDS需要修改时,会检查SDS的 free 空间是否满足要求,如果不满足会自动扩展空间
减少内存重分配次数
C字符串每修改一次都需要重新分配内存
SDS通过空间预分配和惰性空间释放优化内存分配次数

空间预分配

如果SDS修改之后的长度(len)小于1MB,那么程序分配同样大小的未使用空间(free)
如果SDS修改之后的长度(len)大于等于1MB,那么程序会分配1MB的未使用空间(free)

惰性空间释放

当SDS缩短时,程序并不会立即使用内存重分配回收缩短的字节,而是使用 free 属性将这些字节的数量记录
需要时可以使用sdsfree释放未使用空间,所以不会造成内存浪费

2 链表

应用场景:列表键(List)的底层实现之一、发布与订阅、慢查询、监视器等。

链表的实现

// adlist.h/listNode
typedef struct listNode {
struct listNode *prev; //前置节点
struct listNode *next; //后置节点
void *value; //节点的值
} listNode; // adlist.h/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链表特点

双端链表
无环:头节点的prev和尾节点的next指针都指向NULL
链表节点使用 void* 保存节点值,所以链表可以保存不同类型的值

3 字典

应用场景:哈希键(Hash)的底层实现之一,Redis数据库。

字典的实现

//哈希表
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask; //总等于size-1, 用于计算索引值
unsigned long used; //已有节点数量
} dictht; //哈希表节点
typedef struct dictEntry {
// 键
void *key; //值
union{
void *val;
uint64_t u64;
int64_t s64;
} v; struct dictEntry *next; //指向下一个哈希表节点,形成链表
} dictEntry; //dictType
typedef struct dictType {
//计算哈希值
unsigned int *hashFunction(const void *key); //复制键的函数
void **keyDup(void *privdata, const void *key); //... } dictType; //Redis字典结构
typedef struct dict {
dictType *type; //保存了一簇用于操作特定类型键值对的函数 void *privdata; //需要传给特定类型函数的可选参数 dictht ht[2]; //哈希表 int rehashidx; //rehash索引
}

哈希冲突

当有两个或以上数量的键被分配到哈希表数组的同一个索引上面时,则称发生了冲突。

Redis的哈希表使用链地址法解决键冲突,每个哈希表节点都有一个next指针,多个被分配到同一索引的节点形成一个单向链表。出于速度考虑,程序总数将新节点添加到链表表头的位置,时间复杂度O(1)。

Rehash重新散列

为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩(通过rehash操作实现)。

\[load\_factor = \frac{ht[0].used}{ht[0].size}
\]

Rehash操作步骤

    为字典ht[1]哈希表分配空间,大小取决于要执行的操作和ht[0].used属性值。

    如果是扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2的\(2^n\) (2的n次方幂)
    如果是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的\(2^n\)

    将保存在ht[0]的所有键值对rehash到ht[1]上面:重新计算key的哈希值和索引值,然后将键值对放到ht[1]对应的位置上(这个过程是渐进式的,不是一步完成)。

    当ht[0]包含的所有键值对都迁移到ht[1]后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并新创建一个空白哈希表,为下一次rehash做准备。

渐进式Rehash期间的哈希表操作

删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。
新增(add)只会在ht[1]哈希表上进行,ht[0]不再进行任何添加操作。

4 跳跃表

应用场景:有序集合键(zset)底层实现之一、集群节点用作内部数据结构。

跳跃表的实现

//跳跃表节点
typedef struct zskiplistNode {
//层,每个层带有两个属性:前进指针和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[]; //后退指针
struct zskiplistNode *backward; //分值,节点按分值从小到达排列
double score; //成员对象
robj *pbj;
} zskiplistNode; //跳跃表
typedef struct zskiplist {
/**
* header、tail: 头尾节点
* length: 表中节点的数量(不包括头节点)
* level: 层数最大节点的层数(不包括头节点)
*/
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
}

特性:跳跃表中的节点按照score大小进行排序,当score相同时,节点按照成员对象的大小进行排序。

5 整数集合

应用场景:集合键的底层实现之一(Set)。

当一个集合只包含整数值元素,且这个集合元素数量不多时,Redis会使用整数集合作为集合键的底层实现。

整数集合的实现

// intset.h/intset结构
typedef struct intset {
//编码方式
uint32_t encoding; //集合包含的元素数量
uint32_t length; //集合元素数组
int8_t contents[];
}

整数集合特性

contents数组的真正类型取决于encoding属性值。如果encoding属性的值为INTSET_ENC_INT16,则contents就是一个int16_t类型的数组;如果encoding属性的值为INTSET_ENC_INT32,则contents就是一个int32_t类型的数组等待。
当新元素添加到整数集合时,并且新元素的类型比整数集合现有元素都要长,整数集合需要先升级upgrade。

整数集合升级步骤

    根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
    将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位置上
    将新元素添加至数组

6 压缩列表

应用场景:列表键(List)和哈希键(Hash)的底层实现之一。

当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么就是短字符串,那么Redis会使用ziplist来做列表键的底层实现。
当一个哈希键只包含少量键值对,并且每个键值对的key和value都是小整数值或短字符串,那么Redis会使用ziplist来做哈希键的底层实现。

压缩列表的构成

属性 类型 长度 用途
zlbytes uint32_t 4字节 记录整个压缩列表占用的内存字节数。
zltail uint32_t 4字节 记录压缩列表表尾节点距离起始地址有多少字节。
zllen uint16_t 2字节 记录整个压缩列表包含的节点数量:
当属性值小于65535时,表示的就是整个压缩列表节点的数量;
当属性值等于65535时,节点的真实数量需要遍历才能得到。
entry1 列表节点 不定 压缩列表包含的各个节点,节点的长度由保存的内容决定。
entry2
...
entryN
zlend uint8_t 1字节 特殊值0xFF,用于标记压缩列表的末端。

压缩列表节点Entry的构成

属性 长度 用途
previous_entry_length 1字节或5字节 记录了压缩列表前一个节点的长度。
encoding 1字节、2字节或5字节 记录了当前节点的content属性所保存数据的类型以及长度。
content 由encoding决定 保存节点的值,可以是字节数组或者整数。

压缩列表特性

添加新节点或者删除旧节点,可能会引发连锁更新操作,但是出现的几率非常低。

Redis读书笔记(一)的相关教程结束。

《Redis读书笔记(一).doc》

下载本文的Word格式文档,以方便收藏与打印。