redis作为一个非关系型数据库,因为其速度快、数据结构多、可持久化而被广泛运用在缓存应用中,本文参照Redis的设计与实现一书,对redis的底层数据结构做简单的介绍。
redis有五种数据结构,分别是字符串、列表、哈希、集合、有序集合。接下来对着五种数据结构做简单介绍
Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型(SDS)。
SDS用shs.h/sdshdr结构来表示:
struct sdshdr {
int len; // 记录buf数组中已使用字节的数量;等于SDS所保存字符串的长度
int free; // 记录buf数组中未使用字节的数量
char bug[]; //字节数组,用于保存字符串
}
注解:
SDS的优势:
Redis针对列表中数据量的大小,分别采用了两种实现方式:压缩列表和链表;
当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表建的底层实现。
所有元素长度小于64bytes并且元素数量小于512
压缩是为了节约内存而开发的。
该结构是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |
---|---|---|---|---|---|---|---|
记录整个压缩列表占用的内存字节数 | 记录压缩列表表尾结点距离压缩列表的起始地址有多少字节 | 记录了压缩列表包含的节点数量 | 列表节点 | 列表节点 | … | 列表节点 | 特殊值0xFF(十进制255),用于标记压缩列表的末端 |
当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表建的底层实现。
列表使用adlist.h/list来持有链表:
typedef struct list {
listNode *head; // 表头节点
listNode *tail; // 表尾节点
unsigned long len; //链表所包含的节点数量
listNode node //节点
void *(*dup) (void *ptr) //节点值复制函数
void (*free) (void *ptr) //节点值释放函数
int (*match) (void *ptr, void *key) //节点值对比函数
} list;
list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是用于实现多态链表所需的类型特定函数。
链表节点使用的是adlist.h/listNde结构来表示:
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; //后置节点
void *value; //节点的值
}
多个listNode可以通过prev和next指针组成双端链表。
Redis的链表实现的特性:
当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来实现哈希键的底层实现。(所有元素长度小于64bytes并且元素数量小于512)
当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
Redis字典所使用的哈希表由dict.h/dictht结构定义:
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned long size; //哈希表的大小
unsigned long sizemask;//哈希表大小掩码,用于计算哈希值,总是等于size-1
unsigned long unsed; //该哈希表已有节点的数量
}
table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry 结构的指针,每个底dictEntry结构保存着一个键值对。
typedef struct dictEntry {
void *key; //键
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //指向下个哈希表节点,形成链表(拉链法解决哈希表冲突时使用)
}
Redis针对集合中的数据的大小,有两种存储方式:整数集合和哈希表
保存的是整数值(long)并且数量小于512,就用整数集合来存储。可以保存int16_t,int32_t,int64_t类型的整数值,并且集合中不会出现重复元素
intset的数据结构:
typedef struct intset {
uint32_t encoding; //编码方式
uint32_t length; //集合包含的元素数量
int8_t contents[]; //保存元素的数组
} intset;
整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。
虽然contents[]声明了int8_t类型的数组,但实际上contents[]并不保存任何int8_t类型的数据,真正的数据类型取决于encoding的类型。
encoding类型有以下三种:
Redis对有序集合的数量,分别采用压缩列表和skiplist&dict的来实现
元素数量< 128并且每个元素的长度<64bytes时用压缩列表来存储
每个集合节点使用两个紧邻的ziplist节点保存,第一个节点保存元素成员(member),第二个元素保存元素分值(score).有序集合在压缩列表中按分值从小到达排序
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef {
zskiplist *zsl; //跳表
dict *dict; //字典
}zset;
zset结构中的跳表按照分值从小到大保存了所有集合元素,每个跳表节点都保存一个集合元素。
通过跳跃表,程序可以对zset进行范围型操作,如ZRANK, ZRANGE就是通过跳跃表的API实现的。
skiplist跳表的结构:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
level用于在O(1)的时间复杂度内获取跳表的层高最大的节点的层数量,表头节点的层高并不计算在内。
一个跳表由多个跳表节点构成:
typedef struct zskiplistNode {
struct zskiplistNode *backward; // 后退指针
double score; // 分值
robj *obj; // 成员对象
struct zskiplistLevel { // 层
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
} zskiplistNode;
数据结构与对应的实现
类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |