1. redis数据类型
- redis有五种基本数据类型
- string
- list
- set
- sorted set
- hash
- 这五种基本数据类型其实是
逻辑数据类型
,每种数据类型都有不止一种的实现方式- 逻辑数据类型,规定了它有哪些操作,有哪些特性。
- 如set规定了它无序,不能重复
- 如list规定了它有序,操作队首或队尾时间复杂度为O(1)
- 所以它们都是规定了有哪些特性,至于你到底怎么实现,取决于你。
- 如hash这个数据类型,在元素少的时候底层实现用的是zipList,元素多了用Dict实现。
2. redis数据结构介绍
2.1 SDS
- 它是String的底层实现方式
- SDS是一个结构体,源码如下
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* buf已保存的字符串字节数,不包含结束标示*/
uint8_t alloc; /* buf申请的总的字节数,不包含结束标示*/
unsigned char flags; /* 不同SDS的头类型,用来控制SDS的头大小 */
char buf[];
};
- len:已保存的字节数
- alloc:分配的字节数
- flags:用来标识SDS的头类型
- buf:字节数组,存放真实的数据
2.1.1 SDS的内存预分配
二进制安全的意思就是,不光能存字符串,也可以存图片或者安装包等格式文件,因为SDS的结束标志不是根据\0,而是根据属性len
2.2 IntSet
- IntSet是redis中Set集合的一种实现方式,基于整数数组来实现的。并且具备长度可变,有序等特征
- 结构如下
typedef struct intset {
uint32_t encoding; /* 编码方式,支持存放16位、32位、64位整数*/
uint32_t length; /* 元素个数 */
int8_t contents[]; /* 整数数组,保存集合数据*/
} intset;
- encoding:整数的编码方式,即一个整数所占字节数,如2字节,4字节,8字节
- length:intset元素个数
- contents:整数数组,保存数据的集合
2.2.1 IntSet升级
- redis中使用秉持的最优原则,假如最开始我们往IntSet存储的整数比较小,两个字节就可以存下,那么我们就用两个字节表示一个整数。
- 但是假如我们突然向其中添加一个数字如50000,这时候两个字节无法存储下。那么IntSet会自动升级编码方式为4个字节。
2.2.2 特性
- IntSet可以看做是一个特殊的整数数组
- 它可以根据需要改变自己的元素类型(2字节,4字节,8字节)
- redis会确保IntSet中的元素唯一、有序(有序是为了可以利用二分查找加快查询速度)
- 具备类型升级机制,可以节省内存空间
- 所以当Set中存储的元素都是整数,并且个数不是很多时,它的底层实现就是用IntSet
2.3 Dict
- Dict由三个部分组成,分别是字典(Dict)、哈希表(DictHashTable)、哈希节点(DictEntry)
typedef struct dict {
dictType *type; // dict类型,内置不同的hash函数
void *privdata; // 私有数据,在做特殊hash运算时用
dictht ht[2]; // 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般是空,rehash时使用
long rehashidx; // rehash的进度,-1表示未进行
int16_t pauserehash; // rehash是否暂停,1则暂停,0则继续
} dict;
- type:dict类型,内置不同的hash函数
- privdata:私有数据,在做特殊hash运算时使用
- ht:哈希表数组,一个字典会包含两个哈希表,其中一个是当前数据,另一个一般为空,rehash时使用
- rehashidx:rehash的进度,-1表示此时未进行rehash
- pauserehash:rehash是否暂停,1则暂停,0则继续
typedef struct dictht {
// entry数组
// 数组中保存的是指向entry的指针
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小的掩码,总等于size - 1
unsigned long sizemask;
// entry个数
unsigned long used;
} dictht;
- table:哈希表中的哈希数组
- size:哈希表中的数组大小
- sizemask:哈希表数组大小的掩码
- used:已经存放的dictEntry个数
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
// 下一个Entry的指针
struct dictEntry *next;
} dictEntry;
- key:键,其实肯定是字符串类型
- v:值,它是一个联合体,可以存任意类型
- next:下一个entry的指针
2.3.1 字典的rehash
- 计算新hash表的realSize,该值决定了当前要做的是扩容还是收缩
- 如果是扩容,则新size为第一个大于等于dict.ht[0].used+1的2的n次方
- 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2的n次方(不得小于4)
- 按照新的realSize申请内存空间,创建dictht,并赋值给dict.ht[1]
- 设置dict.rehashidx=0,标识开始rehash(0标识将要开始进行转移的数组下标)
之后每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidex是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]
- 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
将rehashidx赋值为-1,代表rehash结束
在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空
2.4 ZipList
- ZipList是一种特殊的“双端列表”,由一系列特殊编码的连续内存块组成。可以在任意一段进行压入/弹出操作,并且该操作的时间复杂度为O(1)
每个ZipListEntry都表示集合中一个单个元素
- ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。
2.4.1 ZipListEntry中的encoding编码
2.4.2 ZipList的连锁更新问题
2.4.3 zipList特性
- 压缩列表可以看做一种连续内存空间的“双向链表”
- 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
- 如果列表数据过多,导致链表过长,可能影响查询性能
- 增加或删改较大数据时有可能发生连续更新问题(但是概率很低)
2.5 QuickList
- ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用过多,申请内存效率就会很低
- 所以一般ZipList存储的元素长度和每个元素大小一般不会很大
- QuickList其实是LinkedList和ZipList的结合。它是一个双端链表,只不过这个链表中的每一个节点都是一个ZipList
typedef struct quicklist {
// 头节点指针
quicklistNode *head;
// 尾节点指针
quicklistNode *tail;
// 所有ziplist的entry的数量
unsigned long count;
// ziplists总数量
unsigned long len;
// ziplist的entry上限,默认值 -2
int fill : QL_FILL_BITS; // 首尾不压缩的节点数量
unsigned int compress : QL_COMP_BITS;
// 内存重分配时的书签数量及数组,一般用不到
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
- head:头节点指针
- tail:尾节点指针
- count:存储的总元素个数(所有ziplist的entry之和)
- len:每个ziplist的的限制
- 值为正,则表示每个ziplist允许的entry个数的最大值
- 值为负,则代表了这个ziplist的最大内存大小,如-2代表不能超过8kb (对应关系下去自己去查)
- fill:首位不压缩的节点数量 (为0表示不压缩)
typedef struct quicklistNode {
// 前一个节点指针
struct quicklistNode *prev;
// 下一个节点指针
struct quicklistNode *next;
// 当前节点的ZipList指针
unsigned char *zl;
// 当前节点的ZipList的字节大小
unsigned int sz;
// 当前节点的ZipList的entry个数
unsigned int count : 16;
// 编码方式:1,ZipList; 2,lzf压缩模式
unsigned int encoding : 2;
// 数据容器类型(预留):1,其它;2,ZipList
unsigned int container : 2;
// 是否被解压缩。1:则说明被解压了,将来要重新压缩
unsigned int recompress : 1;
unsigned int attempted_compress : 1; //测试用
unsigned int extra : 10; /*预留字段*/
} quicklistNode;
- prev:前一个节点的指针
- next:后一个节点指针
- zl:ziplist指针
- sz:当前节点的ziplist的字节大小
- count:当前zipList的entry个数
- encoding:编码方式 1表示ziplist方式,2表示压缩的ziplist模式
- container:目前数据的容器类型,2表示ziplist(其他的都是预留,方便扩展)
2.5.1 特性
- QuickList是一个节点为zipList的双端链表
- 节点采用zipList,解决了传统链表的内存占用问题
- 控制了zipList大小,解决连续内存空间申请效率问题
- 中间节点可以压缩,进一步节省了内存
2.6 SkipList
- 跳表首先是链表,但是与传统链表有些差异
- 元素按照升序排列存储
- 节点可能包含多个指针,指针跨度不同
// t_zset.c
typedef struct zskiplist {
// 头尾节点指针
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 最大的索引层级,默认是1
int level;
} zskiplist;
- header,tail: 头尾节点指针
- length:节点数量
- level:最大索引层级。
// t_zset.c
typedef struct zskiplistNode {
sds ele; // 节点存储的值
double score;// 节点分数,排序、查找用
struct zskiplistNode *backward; // 前一个节点指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 下一个节点指针
unsigned long span; // 索引跨度
} level[]; // 多级索引数组
} zskiplistNode;
- ele:节点中存储的值
- score:节点分数
- backward:前一个节点指针
- level:多级索引数组
2.6.1 特性
- 跳跃表是一个双向链表,每个节点都包含score和ele值
- 节点按照score值排序,socre值一样则按照ele字典排序
- 每个节点都可以包含多层指针,层数在1~32之间的随机数
- 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
- 增删改查效率与红黑树一致,实现却更简单
3. RedisObject
- 前面咱们说过了,redis中有五种基本数据类型。其中我们key一直都是String类型
- 其实,在redis中五种数据类型都会被封装为一个RedisObject,也叫作redis对象
- type:对象类型,五种基本数据类型中的一种
- encoding:底层编码方式,包含了11种
- 0:raw编码动态字符串
- 1:long类型的整数字符串
- 2:dict
- 4:linkedList
- 5:zipList
- 6:intset
- 7:skipList
- 8:embstr的动态字符串
- 9:quickList
- 10:stream流
3.1 五种数据类型的编码方式
3.1.1 String
- 有三种编码方式:int,embstr,raw
- 其基本编码方式是
raw
,是基于SDS实现,存储上限是512mb - 如果存储的SDS长度小于44字节,则会采用
embstr
编码,此时Object head与SDS是一段连续的空间。申请内存时只需要调用一次内存分配函数,效率更高。 - 如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码,直接将数据保存在redisObject的ptr指针位置(刚好8字节),这样就不在需要SDS了。
3.1.2 List
- 3.2版本前,通过ziplist和linkedlist来实现List。
- 当元素数量小于512并且元素大小小于64字节使用ziplist编码
- 超过则采用linkedlist编码
- 3.2版本之后,统一使用Quicklist来实现List
3.1.3 Set
- Set要具有以下特点
- 不保证有序性
- 保证元素唯一
- 方便求并集,交集,差集
- 为了方便查询效率和唯一性,set采用HT编码(Dict)。Dict中key用来存储元素,value统一为NULL
- 当存储的所有数据都是整数,并且元素数量没超过set-max-intset-entries时(默认512),Set会采用IntSet编码,以节省内存。
3.1.4 ZSet
- ZSet有以下特点
- 每个元素都需要指定一个score值和member值
- 需要根据score值排序
- member必须唯一
- 能快速根据member查询score
// zset结构
typedef struct zset {
// Dict指针
dict *dict;
// SkipList指针
zskiplist *zsl;
} zset;
- dict:包含一个字典
- zsl:包含一个跳表
- 当元素数量不多时,dict和SkipList的优势不明显,而且更加消耗内存。因此zset会采用zipList结构来节省内存,不过需要同时满足两个条件
- 元素数量小于zset_max_ziplist_entries,默认128
- 每个元素都小于zset_max_ziplist_value字节,默认64
- ziplist本身没有排序功能,而且没有键值对的概念,因此需要zset通过编码实现:
- ziplist是连续内存,因此score和element是紧挨一起的两个entry,element在前,score在后
- score越小越接近队首,score越大越接近队尾,按照score值升序排列
3.1.5 Hash
- Hash需要有以下特点
- 都是键值存储
- 可以根据键快速获取值
- 键必须唯一
- Hash结构默认采用ziplist编码,用以节省内存。ziplist中相邻的两个entry分别保存field和value
- 当数据量较大时,Hash结构会转为HT编码(dict),其出发条件有两个
- ziplist中元素数量超过了hash-max-ziplist-entries(默认512)
- ziplistz中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
根据b站视频总结https://www.bilibili.com/video/BV1cr4y1671t