哈希表 hash
typedef struct dict {
…
//两个Hash表,交替使用,用于rehash操作
dictht ht[2];
…
} dict;
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希表已有的节点数量
unsigned long used;
} dictht;
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
-
链式哈希:相当于数组+链表
-
哈希冲突:单项链表连接起来
-
rehash:防止链表长度过大查找效率低
- 给「哈希表 2」 分配空间,比「哈希表 1」 大 2 倍;
- 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
- 把「哈希表 2」 设置为「哈希表 1」,「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
- 问题:哈希表大,导致大量数据拷贝
-
渐进式rehash:
- 给「哈希表 2」 分配空间;
- 新增、删除、查找或者更新操作时,操作哈希表1,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
- 查找,先「哈希表 1」后哈希表 2
- 当把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
-
rehash 触发条件:负载因子=节点数/哈希表大小
-
=1:没有RDB、AOF,rehash
-
=5:强制rehash
-
整数集合 inset
typedef struct intset {
//编码方式,int16_t 、int32_t 、int64_t
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
-
整数集合的升级:新加入元素超过encoding设置大小
- 不会重新分配一个新类型的数组,在原数组扩展,然将每个元素按间隔类型大小分割
-
不支持降级
好处:
-
节省内存资源,需要更大内存空间类型才进行升级,默认使用int8_t类型;
跳表 zskiplist
优势:平均 O(logN) 复杂度的节点查找,高效范围查找(比红黑树好)。
typedef struct zset {
// 哈希表
dict *dict;
// 跳表
zskiplist *zsl;
} zset;
-
插入或更新,依次在跳表和哈希表中操作,保证信息一致。
- 跳表:负责范围查找
- hash:获取元素权重
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
//Zset 对象的元素值
sds ele;
//元素权重值
double score;
//后向指针
struct zskiplistNode *backward;
//节点的level数组,保存每层上的前向指针和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
- 排位:查找元素经过的跨度之和
跳表节点查询过程
-
从头节点的最高层开始,逐一遍历每一层。根据权重判断:
- 当前节点的权重 < 要查找的权重:访问该层上的下一个节点。
- = 要查找的权重并且当前节点的 SDS 类型数据「小于」要查找的数据:访问该层上的下一个节点。
- 都不满足,或者下一个节点为空:使用下一层指针,相当于跳到了下一层接着查找。
-
相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
-
跳表在创建节点的时候,根据概率随机生成每个节点的层数
- 随机数<0.5,增加一层,以此类推
- ZSKIPLIST_MAXLEVEL 最高的层数,Redis 7.0 为 32,Redis 5.0 为 64,Redis 3.0 为 32。