介绍
- 文中数据和笔记参照《Redis设计与实现》, 书中内容详实仔细、引人入胜, 本文仅做笔记补充使用
- 有些地方掺杂了个人理解和思考, 若有谬误,欢迎斧正
字符串
- redis使用结构体SDS包装了c语言的字符串
- 空间换时间, 降低获取长度的时间复杂度
- 杜绝溢出(其实可以使用c函数strncat解决)
- 二进制安全(因为使用len判断字符串长度, 字符串中的各种特殊字符不会阻碍读取)
- 遵循C中使用’\0’作为字符串结束的标准, 可以重用String.h中的部分函数
struct sdshdr{
int len; //字符串长度(忽略\0)
int free; //数组剩余长度
char buf[]; //实际存储字符串的数组
}
降低空间重分配次数策略
-
空间预分配
- 字符串实际长度 < 1MB时, buf = 2 * realLen +1(储存’\0’)
- 字符串实际长度 >= 1MB时, buf = realLen + 1MB
-
惰性空间释放
- 缩短字符串长度时, 不会释放字符串数组的空间
- 可通过api调用释放空间
链表
- redis中广泛使用双端链表这一数据结构
- 特点
- 双端
- 无环(头尾都指向NULL)
- 有头尾指针
- 有链表计数器
- 多态(void* 指针可以适应各种情况)
// node的结构
typedef struct listNode{
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
// 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;
字典
字典的基本结构
- 每个键独一无二的键值对集合
- 使用hash表作为底层实现
// hash表节点
typedef struct dictEntry{
void *key;
union{ // 值是整数就直接保存, 否则就指针形式保存
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 串联相同hash值的节点
}dictEntry;
// hash表
typedef struct dictht{
dictEntry **table; // 一个指向dictEntry*类指针的指针, 作为指向dictEntry*指针数组的指针
unsigned long size; // 表大小
unsigned long sizemask; // 表大小掩码, 值为size-1
unsigned long used; // 已使用多少节点
}
-
hash表示例
-
字典实现
typedef struct dictType{ // privdata是为了适应多态的可选额外信息
unsigned int (*hashFunction)(const void *key); // 计算hash值函数指针
void *(*keyDup)(void *privdata, const void *key); // 复制键函数指针
void *(*valDup)(void *privdata, const void *obj); // 复制值函数指针
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 对比键函数指针
void *(*keyDestruction)(void *privdata, const void *key); // 销毁键函数指针
void *(*calDestruction)(void *privdata, const void *obj); // 销毁值函数指针
} dictType;
typedef struct dict{
dictType *type; // 一组功能函数
void *privdata; // 可选参数
dictht ht[2]; // 两个hash表, 正常适应ht[0], rehash时使用ht[1]
int trehashidx; // rehash次数, 没有rehash就是-1
} dict;
hash算法
- 新生成的键值对K-V及其结构体dictEntry
- 使用hashFunction计算出K的hash值 h
- 使用MurmurHash作为Function
- 将h与sizemask求与, h & sizemask
- 根据结果将dictEntry挂到table的对应位置
rehash
- 当table中k-v的数量到达标准后会进行rehash
- 扩展, 将ht[1]的大小设置为2n >=
ht[0] * 2
- 收缩, 将ht[1]的大小设置为2n >=
ht[0]
- 扩展, 将ht[1]的大小设置为2n >=
- 将ht[0]上的键值对rehash到ht[1]上
- 释放ht[0]
- 将ht[1]和ht[0]交换
- 在ht[1]创建一张新的空表
渐进式rehash
- 当键值对数量过多时, 一次性rehash会导致服务器宕机
- 操作
- 先同时持有ht[0] 和 ht[1]
- 在每一次对table中项进行增删改查的时候, 就将该项对应那一列项全部转移至ht[1]
- 同时将rehashindex++
- 直到全部转移后交换ht[0] 和 ht[1]
- 并将rehashindex归为-1
- tips
- 删改查table时先搜索ht[0], 再搜索ht[1]
- 新增操作全部用于ht[1]
rehash的时机
- hashTable的size其实并不能实际限制表的存储数量, 更像是一种预期
- load_factor(负载因子) = ht[0].used / ht[0].size
- 扩展
- 当服务器未执行BGSAVE或BGREWRITEAOF且load_factor > 1时扩展
- 当服务器执行BGSAVE或BGREWRITEAOF且load_factor > 5时扩展
- 尽可能减少执行这两个命令时的内存消耗
- 收缩
- load_factor < 0.1时收缩
跳跃表
- 通过维护多个指针达到快速访问节点的目的(代替复杂的平衡树)
- 时间复杂度: 平均O(logN), 最坏O(N)
- 是redis中有序集合的实现之一
- 有序集合中元素多、元素的成员是长字符串时使用跳跃表
- 有序集合中每个元素有一个分值, 通过分值确定顺序
// 跳跃表节点
typedef struct zskiplistNode{
// 层 -- 一组指向后不同步长的指针, 随机初始化层高在1 ~ 32之间(越大随机到的几率越小)
struct zskiplistLevel{
struct zskiplistNode *forward;
unsigned int span;
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
// 跳跃表
typedef struct zskiplist{
zskiplistNode *header, *tail; // 头尾指针
unsigned long length; // 节点数
int level; // 层数最大节点的层数
}
- 跳跃表示意图
整数集合
- 集合中元素数量较少且只包含整数时作为实现手段
typedef struct intset{
uint32_t encoding; // 编码方式
uint32_t length; // 集合的元素数量
int8_t contents[]; //保存元素的数组, 从大到小排列
} intset;
-
contents中元素的数据类型不取决与int8_t, 而是取决于encoding的值
- INTSET_ENC_INT16
- INTSET_ENC_INT32
- INTSET_ENC_INT64
-
默认的encoding的类型是int16, 每次插入的数据规模大于现在的encoding类型时就会引发升级(不会降级)
- 将encoding向上调一级
- 扩大contents的容量使其匹配类型
- 将之前的元素挪到正确的位置
- 插入新元素(在头或者在尾)
-
好处
- 提升灵活性(可以添加任意类型的整数)
- 节省内存(只在需要升级时升级)
压缩列表(ziplist)
- 应用场景
- 一个只包含小整数和较短字符串的列表(新版redis使用的是quicklist)
- quicklist: 将多个ziplist做成双向链表
- 一个键和值都是小整数或较短字符串的哈希表
- 一个只包含小整数和较短字符串的列表(新版redis使用的是quicklist)
- 实现
- 实质是linkedlist的一种
- 每一个entry可以保存一个整数或者不定长的字符串(字符数组形式保存)
- zltail是首节点到尾结点的偏移量(快速定位到最后一个节点)
表大小 | 偏移量 | 节点数 | 节点实体 | 表尾标记 | |
---|---|---|---|---|---|
zlbytes | zltail | zllen | entry1 | … | zlend |
- entry中的content可以是长度分别为(26-1, 214-1, 232-1)的字节数组或六种不同长度整数的一种
前一节点长度 | content的长度 | 实际保存的内容 |
---|---|---|
previous_entry_length | encoding | content |
- previous_entry_length可以为1字节(前节点长度 < 254字节)或者5字节(首字节值为254)
- 可以通过当前节点的位置 和 previous_entry_length算出前一节点的位置
- 连锁更新(性能隐患)
- 当有多个连续且大小处于250 ~ 253字节之间的节点时
- 在这些节点前插入一个大小 > 254字节的节点, 会导致previous_entry_length长度 + 4
- 从而引起一串的地址和空间重新分配问题
- 还好这并不常见