“这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
Redis底层数据结构
在青训营课程中,缓存是提升系统性能的重要组件,Redis作为缓存中间件中的重要一员,今天我们来聊聊Redis中的底层数据结构。
1.SDS
简单动态字符串,是Redis所构建的一个抽象类型,主要有两个作用
- 默认的字符串保存方式
- 缓冲区(AOF的AOF缓冲区就使用它)
1.1 定义
struct sdshdr{
int len; //记录buf数组中已使用字节的数量,是SDS的长度
int free; //记录buf中未使用的字符串数量
char buf[]; //用来保存字符串
}
1.2 SDS与C字符串区别
- O(1)时间复杂度获取长度,也就是使用
sdshdr->len属性获取 - 杜绝缓冲区溢出,当进行字符串修改的时候,会先判断SDS的空间是否满足修改所需的要求,如果不满足,API自动将SDS的空间扩展至所需的大小,然后进行修改。
- 减少了修改字符串重分配次数,使用了空间预分配和惰性空间释放两种优化策略。
- 二进制安全,C字符串只能保存符合ASCII编码的文本数据,对字符串结尾的识别只有‘\0’,也就是说如果字符串buf数组包含
'\0',那么而SDS是通过sdshdr->len来获取字符串长度。
空间预分配:当对SDS进行扩展的时候,程序不仅会为SDS分配所必须的空间,还会为SDS分配未使用的空间。
- 对SDS修改后,如果长度小于
1MB,那么就会free属性就会分配和len属性同样大小的长度 - 对SDS修改后,如果长度大于
1MB,那么会额外分配1MB的空间。
惰性空间释放:当需要缩点SDS字符长度的时候,不会立即释放空闲的字符串,而是会使用free记录起来。
2.链表
2.1 定义
每个链表节点由adlist.h/listNode定义
type struct listNode{
struct listNode *prev;
struct listNode *tail;
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) //节点值比对函数
}
- 双端无环
- 获取长度是O(1)的
- 获取表头结点和表尾节点是O(1)的
- 多态:使用void*指针来保存节点值,并通过list结构的dup、free、match来保存不同的值。
3.字典
字典是保存键值对的抽象数据结构。
3.1 定义
//哈希表
typedef struct dicht{
//用来保存 Entry 指针的数组
dictEntry **table;
//哈希表的大小
unsigned long size;
//哈希表的大小掩码,用于计算索引值, 总是等于size-1
unsigned long sizemark;
//该hash表的已有节点的数量
unsigned long used;
}dicht
typedef struct dictEntry{
void *key;
//val
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//hash冲突的时候,指向下个节点的指针
struct dictEntry *next;
}
typedef struct dict{
//类型特定函数,type包含了该类型的一些api
dictType *type;
//保存了穿给特定函数的可选参数
void *privdata;
//hash表的个数,ht[1]只有在ht[0] rehash的时候才会使用
dictht ht[2];
//rehash索引, 初始值为 -1
int hashidx;
}
3.2 哈希算法
通过哈希值和掩码来确定索引值。具体使用MurmurHash2算法来确定。
hash = dict->type->hashFunction(key); //获取key对应的hash值
index = hash & (dict->ht[x].sizemask); //获取索引值
3.3 哈希冲突
使用拉链发来存储,通过头插法来实现
3-4 rehash
在重新散列过程中,将ht[0]的数据rehash到ht[1],在rehash过程是渐进式的,具体步骤如下:
- 为ht[1]分配空间
- 首先将
hashidx=0,代表rehash开始 - 对于每个哈希表的
hashidx位置,将其重新hash到ht[1] - 全部rehash完成
问题一:为ht[1]分配多少空间?
- 如果是扩展操作,那么应该是第一个大于等于
ht[0].used*2的 - 如果是删除操作,那么应该是第一个大于等于
ht[0].used的
问题二:rehash期间的哈希表操作?
增删改查操作会首先去ht[0]寻找,如果未找到那么去ht[1]寻找。新增的键值对都会保存到ht[1]里面,而ht[0]不再进行任何添加操作。
4.跳跃表
跳跃表由redis.h/zsiplistNode和redis.h/zskiplist两个结构定义。
typedef struct zskiplistNode {
struct zskiplistLevel{
struct zskiplistNode *forward;
unsigned int span;
}level[];//节点的层
double score;//节点的权重
struct zskiplistNode *backward; //回退指针
robj *obj;//成员对象
}zskiplistNode;
//用于管理节点
typedef struct zskiplist{
struct zskiplistNode *head, *tail;
unsigned long length;//节点的数量
int level; //最大层数
}
在生成节点的时候,会根据幂次定律随机生成一个介于1~31之间的值作为level层的大小,这也是层的高度。
当查找的时候,会按照下述步骤进行查找:
- 首先从最高level的第一节点出发
- 遍历该层的所有节点,该level节点的
forward也就是下一节点大于target,那么就会下降一层 - 下降完成后,继续操作2,直到找到为止
5.压缩链表
5.1 压缩列表组成
压缩列表是列表键和哈希键的底层实现之一。
| zlbytes | zltail | zllen | entry1 | entry2 | …. | zlend |
|---|---|---|---|---|---|---|
| 记录整个压缩链表的所占字节数 | 记录链表表尾节点距离压缩链表的起始位置由多少字节 | 记录压缩链表的节点数量 | 节点1 | 节点2 | 标记压缩列表的末端 |
5.1 压缩列表节点Entry组成
| previous_entry_length | encoding | content |
|---|---|---|
| 记录压缩列表前一个节点的长度大小 | 此节点记录content属所保存的类型和长度 | 存放字节数组或者整数 |
previous_entry_length:我们可以通过zltail拿到尾部节点,然后尾节点地址减去前一个长度得到前一个节点的首部地址,以此类推,可以遍历整个列表。
encoding编码格式:
- 如果content存放的是节点数组,那么编码前两位为
00,01,10 - 如果content存放的是整数,那么编码是以开头为
11的整数编码
重要总结
- Redis数据库中的每个键值对的键和值都是一个对象。
- Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,对象所占用内存就会自动释放。
- Redis会共享值为 0-9999的字符串对象
- 对象会记录自己最后一次被访问的时间,可以用来计算空转时长。