REDIS底层数据结构
一.redis的字符串
redis底层使用SDS(simple dynamic string) 作为默认字符串表示.SDS还被用作缓冲区,AOF模块的缓冲区以及客户端状态中的输入缓冲区.
1.1 SDS的定义
struct sdshdr{
int len;//字符串长度
int free;//数组中未使用的长度
char buff[];//字符数组
}
与C语言的字符串相比,SDS获取字符串长度的时间复杂度为o(1),SDS还杜绝了缓冲区溢出的情况。
1.2 SDS的空间分配优化策略
1.2.1 空间预分配
如果对SDS进行修改,长度小于1MB,那么程序分配和len属性同样大小的空间.如果大于1M,那么会分配1MB的未使用空间.
1.2.2 惰性空间释放
1.3 二进制安全
C语言默认以空字符结尾,所以不能保存图片,音频等,SDS通过len属性来判断是否到结尾了,所以可以保存二进制数据.
二.redis的链表结构
2.1 链表的数据结构
typedef struct listNode{
listNode *head;
listNode *tail;
unsigned long len;//节点数量
void * dup(void * ptr)//复制函数
void * free(void * ptr)//释放函数
void * match(void * ptr,void * key)//对比函数
}
typedef struct listNode{
struct listNode *prev;
struct listNode * next;
void * value;//节点的值
}
列表被广泛用于实现redis的各种功能,比如列表键,发布于订阅,慢查询,监视器.
三.redis的字典结构(SET)
字典使用哈希表作为底层的实现.
3.1 哈希表的数据结构
typede struct dictht{
dicEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemark;//掩码用于计算索引值
unsigned long used;//已有节点的数量
}
table是一个数组,每个元素都指向一个dictEntry结构的指针,每个dicEntry保存一个键值对
3.1.1 哈希表节点
typedef struct dictEntry{
void *key;
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry * next;
}
3.1.2 字典
typede struct dict{
dictType *type;
void * privdata;//私有数据
dicttht ht[2];//哈希表
//rehash索引,当rehash不在进行时,值为-1
int rehashidx;
}
一般情况下,只用ht[0]哈希表,ht[1]只会在rehash时使用.rehashidx 记录目前rehash的进度.因为dictEntry节点组成的链表没有指向链表表尾的指针,所以总是把新节点添加到链表的表头位置.
当哈希表的键值对过多或者过少,需要对哈希表进行rehash.
为字典ht[1]分配空间
- 如果是扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2的2的n次方,
- 如果是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2的n次方.
- 将所有ht[0]的键值对rehash到ht[1]上
- 当ht[0]的所有键值对都rehash完毕,释放ht[0]的空间,将ht[1]设置成ht[0],并在ht[1]创建一个空的哈希表.
-
3.1.3 rehash的条件
- 服务器目前没有在进行bgsave或者bgrewriteaof,并且负载因子大于等于1
- 服务器目前在进行bgsave或者bgrewriteaof,并且负载因子大于等于5
- 为ht[1]分配空间,让字典同时拥有两个哈希表
- 把rehashidx值设置为0,表示正在进行rehash
- 在rehash期间,对字典的操作都会映射到两个哈希表,同时还会顺带奖ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],rehash完成时,rehashidx自动加一.
- 随着字典操作的不断执行,最终在某个时间点ht[0]的所有键值对会被全部rehash到ht[1]上,这时,rehashidx被设置成-1,表示rehash完成.
-
四.redis的跳跃表(skiplist)
4.1跳表的实现
最左边的结构是zskiplist:
header:指向跳表表头
tail:指向跳表表尾
. level:代表跳表的层数.
length:代表跳表的节点数量
右边的代表zskiplistnode结构:
层:节点中L1代表第一层,L2代表第二次,以此类推
分值(score):各个节点的值,按从小到大排序
成员对象(obj):o1,o2,o3是节点保存的成员变量
3.2跳表的数据结构
typedef struct zskiplistNode {
robj *obj; //节点数据
double score;
struct zskiplistNode*backward; //后退指针
struct zskiplistLevel {
struct zskiplistNode*forward;//前进指针
unsigned int span;//该层跨越的节点数量
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode*header, *tail;
unsigned long length;//节点的数目
int level;//目前表的最大层数
} zskiplist;
(PS:每个跳表节点的层高都在0-32之间)
整数集合是集合键的底层实现之一,当一个集合只包含整数元素,并且数量不多的时候,会作为集合键的底层实现.
typedef struct intset {
/*
虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组的真正类型取决于 encoding 属性的值:
如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t类型的整数值
(最小值为 -32,768 ,最大值为 32,767 )。
如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t类型的整数值
(最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t类型的整数值
(最小值为 -9,223,372,036,854,775,808 ,最大值为9,223,372,036,854,775,807 )。
*/
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
如果新插入的元素比现有类型的encoding属性的值要长,就要进行升级.
1.根据新元素的类型,重新计算并分配空间
2.将所有元素都转换成新元素相同的类型,并重新放置.
ps:整数集合不支持降级操作
六.压缩列表(ziplist)
ziplist是列表键和哈希键的底层实现之一.如果数据量少并且都是小整数值或者长度较短的字符串,那么redis就用它作为列表键的底层实现.
6.1 压缩列表的构成
压缩列表是为了节约内存而开发的.一个压缩列表可以包含任意多个节点,每个节点保存一个字节数组或者一个整数值.
属性 | 类型 | 长度 | 用途 |
zlbytes | uint32_t | 4字节 | 计算整个压缩列表占用的内存字节数 |
zltail | uint32_t | 4字节 | 记录压缩列表表尾节点距离起始地址有多少字节:上图中p代表起始节点的指针,加上(0x3c=60)就是表尾节点的位置. |
zllen | uint16_t | 2字节 | 记录节点数量,如果数量大于2^16,就需要遍历整个列表才能知道数量了. |
entry | 不定 | ||
zlend | uint8_t | 1字节 | (0xFF),用于标记压缩列表的末端 |
6.2压缩列表节点的结构
属性 | 类型 | 长度 | 用途 |
previoud_entry_length | 1~5个字节 | 前一个节点的长度 | |
encoding | 1,2,5字节 | 记录节点content属性保存的类型以及长度 | |
content | 保存节点内容 |