Redis读书笔记(一)
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操作实现)。
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决定 | 保存节点的值,可以是字节数组或者整数。 |
压缩列表特性
- 添加新节点或者删除旧节点,可能会引发连锁更新操作,但是出现的几率非常低。