基本概念
Redis是一个基于内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。Redis支持五种常见的对象类型:
- 字符串(String)
- 哈希(Hash)
- 列表(List)
- 集合(Set)
- 有序集合(Zset)
对象类型与编码
Redis 使用对象来存储键和值,在Redis中,每个对象是由redisObject结构表示。redisObject结构主要包含三个属性:type、encoding 和 ptr。
typedef struct redisObject {
//记录对象数据类型
unsigned type:4;
//对应的编码方式
unsigned encoding:4;
//对象的底层数据结构
void *ptr;
} robj;
常说Redis采用哪种对象类型,是指对应的值采用哪种数据类型。
之所以用encoding属性来决定对象的底层数据结构,是为了实现同一种对象类型,支持不同的底层实现。这样做的好处是,可以根据具体的场景,选取不同的编码方式,使用不同的数据结构,可以提高redis数据库的灵活性和效率。
字符串对象
非常常用的一种对象类型,像上面提及的那样,它可以有int、raw和embstr三种编码方式。
当字符串对象保存值的长度是一个不超过long类型的整数值时,编码方式为int,底层数据结构直接是long类型;
当字符串保存值的长度是一个大于等于39字节的字符串时,对应的编码类型为raw,其底层数据结构为简单动态字符(SDS);
当字符串保存值的长度是一个小于39字节的字符串时,编码类型为embstr,底层数据就是embstr编码SDS;
SDS相比较C字符串的不同点
- 常数复杂度获取字符串长度
SDS采用len属性来记录每个字符串的长度,因此,获得SDS字符串的长度时间复杂度为O(1)。
- 杜绝缓存区溢出
在SDS进行字符串扩充时,首先会检查当前字符数组的长度是否足够。不够的话,要进行扩容操作。
- 减少修改字符串是带来的内存分配次数
增长或者缩短字符串时,都需要进行内存重分配来进行操作,避免了内存泄漏和内存溢出问题。
空间预分配和惰性空间释放两种优化
空间预分配(增长字符串操作)
字节数组空间不足时,总能预留出来一部分空间,这样能减少连续执行字符串增长操作时的内存重分配次数。
a、当len小于1MB时,每次重分配同样大小的空闲空间
b、当len大于1MB时,每次重分配会多分配1MB的空闲空间
惰性空间释放(缩短字符串)
并不会在字符串缩短时,立即使用内存重分配来回收内存空间,而是用free记录下来,等待将来扩容时在去使用这部分内存。
- 二进制安全
SDS做到了二进制安全,使得Redis可以保存二进制数据
列表对象
类标编码的对象可以是linkedlist或者ziplist,对应的数据结构分别是链表和压缩链表。
编码方式
当字符串长度小于64字节时,切元素个数小于512个时,列表采用ziplist编码,否则采用linkedlist编码。
对应节点数据结构
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);
链表特点
- 双端:*prev, *tail
- 无环:prev => NULL;表尾节点next => NULL
- 获取表头指针和表尾指针的复杂度为O(1)
- 带有链表长度计数器
- 多态
压缩列表
列表键和哈希键的底层实现之一。它是由一系列特殊编码的连续内存块组成的顺序性数据结构。
哈希对象
编码方式:ziplist 和 hashtable
hash-ziplist
将每次保存的键值值压入压缩列表的表尾,先键后值。
hash-hashtable
使用字典作为底层实现。字典适用于保存键值对的一种数据结构。
哈希表
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//哈希表已有节点数量
unsigned long used;
} dictht
table属性是一个数组,每个元素对应指向dictEntry结构的指针。
typedef struct dictEntry {
void *key;
union {
void *val;
unit64_t u64;
nit64_t s64;
} v;
struct dictEntry *next;
} dictEntry;
字典
typedef struct dict {
dictType *type;
void *privdata;
//哈希表
//使用ht[0]哈希表,ht[1]用于在进行rehash时使用
dictht hrt[2];
//rehash索引
int rehashidx;
}
rehash
使得负载因子 ht[0]).used / ht[0]).size 维持在一个合理范围,在哈希表元素过多或者过少时,需要对hash表进行相应的扩展与收缩。
步骤
为 ht[1] 分配空间
1、扩展:大小为第一个大于 ht[0]).used*2的2n
2、收缩:大小为第一个大于 ht[0]).used的2n
3、将 ht[0] 的元素复制rehash到 ht[1] 中
4、将 ht[0] 重命名为 ht[1],方便下一次rehash
渐进式hash
一次迁移一个桶上的所有数据,将原本集中的操作分散到每个添加,删除,查找和更新上,使用到了分治算法。
集合对象
set-intset
对应编码方式:intset或者hashtable(元素个数>=512)
整数集合的数据结构
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
整数集合的类型升级
1、扩展整数集合底层数组空间的大小;
2、将底层数组现有的所有元素都转移为新元素相同的类型,并维持底层数组的有序性;
3、将新元素添加到底层数组里面。
整数集合不支持类型降级
set-hashtable
使用字典作为其底层实现
有序集合
编码方式:ziplist(元素个数 <= 128 && 长度 <= 64字节) 和 skiplist(跳跃表)
zset-ziplist
使用压缩列表作为底层实现,集合中的元素会根据分值的大小从小到大排列。
zset-skiplist
skiplist编码的有序集合对象使用zset结构作为其底层实现
typedef struct zset {
zskiplist *zsl;
dict *dict;
}
跳跃表数据结构
跳跃表节点zskiplistNode
typedef strcut zskiplist {
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层级
struct zskiplistLevel {
//前进指针
struct zskiplistNode *foreward;
//跨度
unsigned int span;
} level[];
} zskiplistNode;