Redis底层数据结构及基本类型的实现
底层数据结构
简单动态字符串(SDS)
数据结构
redis中用一种名为简单动态字符串(simple dynamic string)的抽象类型作为默认字符串表示,如在命令set name sher
中,在redis数据中创建了新的键值对 name-sher
,而在底层就是存储了保存着name
的SDS和sher
的SDS,除了保存redis数据库中的字符串值外,SDS还用作缓冲区(AOF持久化中的AOF缓冲区、客户端中的输入缓冲区都是由SDS实现),SDS底层是这样的数据结构:
struct sdshdr {
// buf 中已占用空间的长度 等于SDS所保存的字符串长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
也就是说与传统的字符串相比,SDS由于在len
属性中记录了其存储的字符串长度,所以获取字符串长度时不需要遍历整个字符串,而是直接获取其len
属性即可,所以获取SDS存储的字符串的长度时间复杂度为O(1)(而获取C字符串的长度时间复杂度为O(n)),而在SDD中也是由\0
作为字符串的结束:
在SDS中,存储数据的buf
数组不一定是字符的数量加一(加一为\0
),而是还包括未使用的字节,这些字节的长度由free
属性记录。
C字符串的问题以及SDS的解决方案
C字符中出现的问题:
由于C字符串中不记录自己的长度,所以会出现内存溢出和内存泄漏:
- 如果将C字符串后面进行拼接操作,如果没有提前分配好内存,很可能就会将原字符串地址后存储的数据进行覆盖,造成内存溢出;
- 如果将C字符串进行缩短操作,原字符串后面不再使用的空间如果没有进行释放,就会造成内存泄漏。
SDS中的解决方案:
-
空间预分配:该策略用于优化字符串的增长操作,其策略用伪代码表示为:
if (len < 0.5MB) buf.length = buf.length + len else buf.length = buf.length + 1MB
也就是如果对SDS进行修改之后,其长度若小于1MB,则分配给和
len
属性同样大小的未使用空间,(分配后SDS中len
属性的值和free
值相同);如果对SDS进行修改后,其长度将大于1MB,则分配个1MB的空间。也就是在扩展SDS的buf
数组前,会先检查未使用空间是否够用,够用的话就不会执行内存分配了,这样就减少了内存分配的次数。 -
惰性空间释放:该策略用于优化字符串的缩短操作,当要缩短SDS字符串时,并不会立即回收缩短后多出来的字节,而是将这些空出来的字节用
free
属性记录下来,并等待以后使用。
链表(linkedlist)
数据结构
Redis基本类型之一的链表键的实现方式之一就是链表这种数据结构,除了用来作为链表键的底层数据结构外,链表还用作**”发布与订阅“、”慢查询“、”监视器“等功能的实现,在Redis中的链表是一种双向链表**,首先看下链表底层实现的整体结构:
然后看其源码数据结构定义如下:
/*
* 双端链表节点
*/
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点的值
} listNode;
/*
* 双端链表结构
*/
typedef struct list {
listNode *head;// 表头节点
listNode *tail;// 表尾节点
void *(*dup)(void *ptr);// 节点值复制函数
void (*free)(void *ptr);// 节点值释放函数
int (*match)(void *ptr, void *key);// 节点值对比函数
unsigned long len;// 链表所包含的节点数量
} list;
特点
通过上面的图例以及数据结构的源码我们可以知道Redis中的双端链表具有以下性质:
- 无环,表头节点的
prev
指针和表尾节点的next
指针都指向null
,链表的访问以null
为终点; - 带表头指针和表尾指针(
list
结构中定义),获取头结点和尾结点的时间复杂度为O(1); - 带链表长度计数器(
list
结构中的len
字段),获取链表节点数量的时间复杂度为O(1); - 由于链表的节点存放
value
字段为一个指针,所以链表实际可以存储各种数据,即数据多态性。
字典(dict)
数据结构
字典(映射、符号表、关联数组)就是一种保存键值对key-value
的抽象数据结构,在Redis中其应用较为广泛,比如Redis中的数据库就是使用字典来作为底层实现的,因为Redis中的每条记录本身就是个键值对;除了用字典表示数据库外,当一个哈希键key
包含的键值对field-value
较多时,Redis也会使用字典作为hash
的底层实现。首先我们看在普通状态下的字典结构如下:
我们发现在字典中存在两个哈希表,通常情况下,我们只会用ht[0]
这个哈希表,那么ht[1]
是用来做什么的?ht[1]
哈希表只会在对ht[0]
哈希表进行rehash
时使用。而在哈希表中存放了哈希表节点的数组,而哈希表节点的数组存放了每个哈希表节点,在哈希表节点中就是真正的键值对数据,这几个数据结构在Redis底层源码实现如下:
/*
* 字典
*/
typedef struct dict {
dictType *type;// 类型特定函数,如计算哈希值,复制、对比键值的函数等
void *privdata;// 私有数据
dictht ht[2];// 哈希表
int rehashidx; /* 当 rehash 不在进行时,值为 -1 */
} dict;
对于字典,type
和privdata
属性是为创建多态字典有关的,这里不详细进行介绍,而**dictth ht[2]
字段恰恰就是存储了用来存储键值对数据的哈希表和用来rehash
的哈希表**,rehashidx
字段在注释中已经说明其用途。
/*
* 哈希表
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
dictEntry **table;// 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
unsigned long used;// 该哈希表已有节点的数量
} dictht;
对于哈希表,其sizemask
和哈希表节点计算出的哈希值共同决定了一个键应该被放到table
数组的哪个索引上。
/*
* 哈希表节点
*/
typedef struct dictEntry {
void *key; // 键
union {
// 值为以下三种结构的之一
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表,解决哈希冲突
} dictEntry;
阅读其源码可知,在Redis中,键值对的值可以是一个指针,也可以是一个uint64_t
整数,也可以是一个int64_t
整数,而在哈希表节点中有一个**next
指针,只是为了将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突问题**。
Redis中采用的哈希算法
当将一个新值添加到字典结构中时,程序根据要加入键值对的键计算出哈希值和索引值,然后再根据索引值,将新的哈希表节点(即要加入的键值对)放到哈希表数组(ht[0]
或ht[1]
,根据是否在rehash)指定索引上。即如下的计算哈希值和索引值的方法:
# 使用字典type字段存储的计算哈希值的函数计算出哈希值
hash = dict->type->hashFunction(key); //作为hash键底层时,用MurmurHash2算法
# 使用哈希表中的sizemask属性和哈希值,计算出索引值
index