Redis底层数据结构
1. SDS
Redis没有使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS),并将SDS用作Redis的默认字符串表示。
struct sdshdr {
// 记录SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
SDS的特点:
- 常数复杂度获取字符串长度: SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。
- 杜绝缓冲区溢出: SDS API需要对SDS修改时,如果SDS的空间不满足要求,API会自动对SDS扩容,避免缓冲区溢出。
- 减少修改字符串时带来的内存重分配次数: SDS通过空间预分配和惰性空间释放两种策略,减少修改字符串时的内存重分配次数。
- 二进制安全: 不像C字符串只能保存文本数据,SDS可以保存像图片,音频,视频,压缩文件这样的二进制数据。
2.链表
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}
Redis的链表实现是双端列表,每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针。链表被广泛用于实现Redis的各种功能,比如列表键,发布与订阅,慢查询,监视器等。
3.map
Redis的字典使用哈希表作为底层实现。
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表已有节点数量
unsigned long used;
}
typedef sturct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
}v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
- 字典被广泛用于实现Redis的各种功能,包括数据库和哈希键。
- 哈希表使用链地址法解决哈希冲突。
- 在对哈希表进行扩容或者缩容时,rehash过程并不是一次性地完成的,程序渐渐式地将现有哈希表所包含的键值对rehash到新哈希表里面。
4.跳表
- 跳表可以看成是有序列表加了索引。
- 跳表是有序集合的底层实现之一。
5.整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
redis> SADD numbers 1 3 5 7 9
(integer) 5
redis> OBJECT ENCODING numbers
"intset"
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
- 整数集合是集合键的底层实现之一。
- 整数集合的底层实现为数组,这个数组以有序,无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
- 升级操作(扩容)为整数集合带来了操作上的便利性,并且尽可能地节省了内存;整数集合只支持升级,不支持降级。
6.压缩列表
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型内存数据结构。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
zlbytes记录整个压缩列表占用的内存字节数。
zltail记录压缩列表表尾节点距离压缩列表的起始地址有多少字节。
zllen记录压缩列表包含的节点数量。
- 压缩列表是一种为节约内存而开发的顺序型数据结构。
- 压缩列表被用作列表键和哈希键的底层实现之一。
- 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
- 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作。
Redis数据类型实现
对于Redis保存的键值对来说,键总是一个字符串对象,而值可以是字符串对象,列表对象,哈希对象,集合对象和有序集合对象中的一种。
1.String
2.List
list可以由双向链表或者压缩列表实现。
redis> RPUSH numbers 1 "three" 5
(integer)3
压缩列表实现的list示意图:
zlbytes | zltail | zllen | 1 | “three” | 3 | zlend |
当数据量比较小时,list会采用压缩列表实现;当数据量比较大时,list会采用双向链表实现。可以通过配置项修改临界值的大小。list-max-ziplist-value
和list-max-ziplist-entries
。
3.Hash
Hash可以由压缩列表或者哈希表实现。具体采用那种实现取决于数据的大小。小数据使用压缩列表。
用压缩列表实现Hash,当有新的键值对要加入Hash对象时,程序会先将保存了键的压缩列表节点推入到压缩列表尾部,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
redis> HSET profile name "Tom"
(integer)1
redis> HSET profile age 25
(integer)1
压缩列表实现的Hash示意图:
zlbytes | zltail | zllen | “name” | “Tom” | “age” | 25 | zlend |
4.set
set可以由整数集合或者哈希表实现。
redis> SADD numbers 1 3 5
(integer)3
1 | 3 | 5 |
哈希表用作集合的底层实现时,哈希表的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而哈希表的值则全部被设置为NULL。
5.Sorted set|
有序列表可以使用压缩列表或者跳表实现。
使用压缩列表实现Sorted set时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
redis> ZADD price 8 apple 6.0 banana 7 cherry
(integer)3
zlbytes | zltail | zllen | “banana” | 6.0 | “cherry” | 7 | “apple” | 8 | zlend |
压缩列表实现Sorted set时,使用zset结构作为底层实现。
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
zset结构中的zsl跳跃表按分值大小保存了所有集合元素键,除此之外,zset结构中的dict字段为有序集合创建了一个从成员到分值的映射。通过哈希表,程序可以用O(1)的复杂度查找给定成员的分值。
为什么有序集合需要同时使用跳跃表和哈希表实现:因为性能。单个元素查找时哈希表更快,范围型操作跳跃表更快。