redis一共有五大经常使用的对象,用type命令便可查看当前键对应的对象类型,分别是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),可是这些只是对外的数据结构,实际上每个对象都有两到三种不一样底层数据结构实现,能够经过object encoding命令查看键值对应的底层数据结构实现,redis
下表即为每种对象所对应的底层数据结构实现。算法
类型 | 编码 | 底层数据结构 |
---|---|---|
string | int | 整数值 |
string | raw | 简单动态字符串 |
string | embstr | 用embstr编码的简单动态字符串 |
hash | ziplist | 压缩列表 |
hash | hashtable | 字典 |
list | ziplist | 压缩列表 |
list | linkedlist | 双端列表 |
set | intset | 整数集合 |
set | hashtable | 字典 |
zset | ziplist | 压缩列表 |
zset | skiplist | 跳表和字典 |
redis并无使用C字符串,而是使用了名为简单动态字符串(SDS)的结构,SDS的定义以下:数组
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
复制代码
那么redis为何要使用看起来更占空间的SDS结构呢?主要有如下几个缘由:安全
双端列表做为一种经常使用的数据结构,当一个list的长度超过512时,那么redis将使用双端列表做为底层数据结构。下面是一个列表节点的定义:bash
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);
} list;
复制代码
能够看到双端列表是一个无环双端带表头表尾节点的链表。函数
散列表(Hash table,也叫哈希表),是根据键而直接访问在内存存储位置的数据结构。也就是说,它经过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表。性能
当hashtable的类型没法知足ziplist的条件时(元素类型小于512且全部值都小于64字节时),redis会使用字典做为hashtable的底层数据结构实现。redis的字典(dict)中维护了两个哈希表(table),而每一个哈希表包含了多个哈希表节点(entry)。下面分别来介绍这三个对象。优化
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,造成链表
struct dictEntry *next;
} dictEntry;
复制代码
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 老是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
复制代码
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
复制代码
当在哈希表中存取数据时,首先须要用hash算法算出键值对中的键所对应的hash值,而后再根据根据table数组的大小取模,计算出对应的索引值,再继续接下来的操做。redis使用了MurmurHash2 算法来计算键的哈希值,又使用了快速幂取模算法下降了取模的复杂度。整个过程以下:ui
hash = dict->type->hashFunction(k0);
index = hash & dict->ht[0].sizemask;
复制代码
当hash冲突发生时则采用链地址法解决hash冲突。
当哈希表保存的键值对愈来愈多时,哈希表的负载因子(load factor = used / size)愈来愈大, 本来O(1)复杂度的查找也会渐渐趋向于O(N),为了保证哈希表的负载因子在必定的范围以内。redis须要动态的调整table数组的大小,其中最重要的即是rehash过程。rehash分如下的几个步骤:
redis的rehash过程并非一次性集中rehash,而是分批间隔式的,在dict中的rehashidx即是为此服务。 相较于一次性的rehash,渐进式的rehash多了下面这些步骤:
这是比较典型的分而治之的思想,将一次性集中做业分散,下降了系统的风险。
跳表的的查找复杂度为平均O(logN)/最坏O(N)。在不少场合下做为替代平衡树的数据结构,在redis中,若是有序集合的属性不知足ziplist的要求,则将跳表做为有序集合的底层实现。
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
复制代码
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
复制代码
跳跃表中保存了头尾节点,方便遍历,还保存了节点的数量,能够在O(1) 复杂度内返回跳跃表的长度。
当集合的值全为整数且集合的长度不超过512时,redis采用整数集合做为集合的底层数据结构。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
复制代码
INTSET_ENC_INT16 , contents 就是一个 int16_t 类型的数组(最小值为 -32,768 ,最大值为 32,767 )。 INTSET_ENC_INT32 , contents 就是一个 int32_t 类型的数组(最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。 INTSET_ENC_INT64 , contents 就是一个 int64_t 类型的数组(最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。
redis采用多种编码的方式,主要仍是为了省内存。当集合中加入了不符合当前集合编码的数字时,数组集合会自动更新至能匹配到的编码,值得注意的是,这种升级是不可逆的,只能由小往大,不能降级。如此一来,就可以在存放小数据时,剩下很大的空间,并且也没必要为编码不匹配的事情而烦恼了。
压缩列表是redis又一个为了节省内存所作的优化,是list/hash/zset的底层数据结构之一,当数据值不大且数量较低时,redis都会使用压缩列表。
压缩列表和双端列表有些相似,不过一个用指针衔接起来,一个则是用数组和长度衔接起来。下面来看一看压缩列表节点的定义:
本文对于redis常见的数据结构及其底层实现进行了分析和梳理,但愿可以理清这些底层数据结构对于redis高性能的做用和影响。