redis底层数据结构实现教程
redis的底层数据结构实现
redis底层数据结构实现
本文章主要整理redis的五种数据类型(string、list、hash、set、zset)的底层数据结构实现。
字典
dictht是一个散列表结构,使用拉链法解决哈希冲突。
This is our hash table structure. Every dictionary has two of this as we implement incremental rehashing, for the old to new table.
typedef struct dictht {
dictEntry **table;
usigned long size;
usigned long sizemask;
usigned long used;
}dictht;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
}v;
struct dictEntry *next;
} dictEntry;
Redis的字典dict中包含两个哈希表dictht,这是为了方便进行rehash操作。在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht的角色。
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; //rehash in progress
usigned long iterators;
} dict;
rehash 操作不是一次性完成,而是采用渐进方式,为了避免一次性执行过多的rehash操作给服务器带来过大的负担。
负载因子 = 哈希表已经保存的节点数量/哈希表大小。
跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:
1、由很多层结构组成;
2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
3、最底层的链表包含了所有的元素;
4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
跳跃表的实现
跳跃表由redis.h/zskiplistNode和redis.h/zkiplist两个结构定义。
- header:指向跳跃表的表头节点
- tail:指向跳跃表的表尾节点
- level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
- length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
- 层(level):节点中用L1、L2、L3等字样标记节点的各个层。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
- 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。
//ZSETs ues a specialized viersion of skiplists
typedef struct zskiplistNode {
robj *obj; //成员对象
double score; //分值
struct zskiplistNode *backward; //后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针
usigned int span; //跨度
} level[];
} zskiplistNode;
节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //header指向跳跃表的表头节点,tail指向跳跃表的表尾节点
unsigned long length; //记录跳跃表内,层数最大的那个节点的层数(表头不计算在内)
int level;
} zskiplist;
SDS(simple dynamic string) 简单的动态字符串
SDS (simple dynamic string)
简单动态字符串
struct sdsndr {
int len; //sds所保存的字符串长度
int free; //buf中没有使用的字节的数量
char buf[]; // 字节数组,用于保存字符串(大小为len+free+1)
}
链表
链表的节点
typedef struct listNode {
struct listNode *prev; //前置节点指针
struct listNode *next; //后置节点指针
void *value;
} listNode;
链表
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;
整数集合
typedef struct insert {
uint32_t encoding; //编码方式
uint32_t length; //集合包含的元素数目
int8_t contents[]; //保存元素的数组
} intset;
压缩列表
压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一。由一系列特殊编码的连续内存块组成的顺序型数据结构。
压缩表原理:将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
zlbyteszltailzllenentry1entry2…entryNzlend- zlbyes: uint32\_t 记录真个压缩表占内存的字节数,用于内存分配或者计算zlend的位置
- zltail: uint32\_t 记录表尾节点举例压缩列表起始地址有多少字节,用于直接确定压缩列表的表尾节点地址
- zllen: uint16\_t 记录压缩列表包含的节点数量,当这个数值等于UINT16\_MAX(65535)时,节点的真实数量需要便利整个压缩列表。
- entryX: 列表节点,压缩列表包含的各个节点
- zlend uint8\_t 特殊值0xFF(255)用于标记压缩列表的末端
压缩列表的每个节点:
previous\_entry\_lengthencodingcontent- previous\_entry\_ength:记录压缩列表前一个字节的长度。previous\_entry\_ength的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。
- encoding:节点的encoding保存的是节点的content的内容类型以及长度,encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长。
- content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。