redis作为最常用的内存型数据库,对外提供了String, List, Hash, Set, Zset 5中数据类型,通过阅读《redis设计与实现》一书,了解了redis数据类型的内部数据结构,在此作一梳理。
对象
Redis作为KV式数据,使用对象来表示数据库中的键和值,每一个键值对都对应两个对象,一个是键对象,一个是值对象。比如执行SET msg "hello, redis"
后,redis将创建两个对象,一个包含"msg"字符串,另一个则包含"hello, redis"。redis中的每个对象都由一个redisObject来表示,该结构中和保存数据有关 的三个属性分别是type属性、encoding属性和ptr属性:
typedef struct redisObject {
//类型
unsigned type: 4;
//编码
unsigned encoding: 4;
//指向底层数据结构的指针
void *ptr;
//...
} robj;
其中type的值对应5中redis数据类型的类型常量(使用TYPE命令可以获取这个值), encoding则记录了底层实现所用的数据结构(使用OBJECT ENCODING命令可以获取这个值),比如List底层可能用ziplist(压缩列表)实现,也可能用list(双向链表)实现。
数据结构
因为redis的每一种数据类型都由其内一种或多种数据结构来实现,因此先看其内部的数据结构,再看其在数据类型中的使用
简单动态字符串
redis实现了一个SDS结构(简单动态字符串)类型来替代C语言中的原生字符串, 结构如下
struct sdshdr {
//记录buf中字符串的实际长度
int len;
//记录buf数组空闲长度
int free;
//字节数组,用于保存字符串
char buf[];
};
和C语言原生字符串相比, 其有一下优点:
- 因为存储了字符串长度,可以常数复杂度获取字符串长度
- 同样由于存储了字符串实际长度和buff空闲长度,字符串变化时不需要每次都重新分配内存, 同时也避免了字符串长度变化可能导致的缓冲区溢出和内存泄漏
- 二进制安全, SDS的设计由于根据字符串长度来判断字符串的末尾,因此中间可以存储任何数据包括空字符
双向链表
redis中的链表类型就是最常用的双向链表,涉及数据结构如下:
//链表节点类型
typedef struct listNode {
struct listNode * prev;
struct listNode * next;
void * value;
}listNode;
//链表类型
typedef struct list {
//表头节点
listNode * head;
//表未节点,方便逆向遍历
listNode * tail;
//节点数目, 优化链表求长度
unsigned long len;
//**
}list;
Hash表
redis中的字典类型也就是基本的hash表, 设计数据结构如下:
typedef struct dictht {
//槽位数组
dictEntry **table;
//槽位数组长度
unsigned long size;
//用于计算索引的掩码
unsigned long sizemask;
//真正存储的键值对数量
unsigned long used;
} dictht;
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_ tu64;
int64_ ts64;
} v;
//指向下一个节点的指针
struct dictEntry *next;
} dictEntry;
其hash算法采用的是MurmurHash算法, 处理冲突使用链地址法。随着操作不断进行,当hash表保存的键值对过多或过少时,需要对hash表的槽位数组进行扩展或收缩以获得更好的性能和内存利用率,这个操作称为rehash。redis中的rehash就是通过创建一个临时的字典,将正在使用的字典中的所有键值对复制到临时字典中(这个过程需要重新计算hash值和索引值,因此称为rehash),最后释放老的字典,并将其指针指向临时字典。当数据量比较小的时候,这个过程可以一次性完成,但是数据量大的时候,庞大的计算量可能导致整个服务停止,因此redis采用了渐进式rehash,就是同时维护两个hash表hd[0]和hd[1],设置rehash_idx设置为0,在0< rehash_idx < 槽位数组长度时, 对hash表的任何操作除了执行正常的操作过程以外,还会将hd[0]槽位数组rehash_idx索引上的所有键值对rehash到hd[1],这个过程中的查找会同时使用两个表进行操作,同时所有的添加键值对操作只会在hd[1]上进行,保证了hd[0]只减不增。rehash完成后,hash表指针指向hd[1],释放hd[0]内存。
跳跃表
跳跃表是一种有序的数据结构,支持平均O(logN),平均O(N)复杂度的节点查找,其效率可以和平衡树媲美,同时实现简单,而且由于不需要reblance操作,在高并发情况下表现更加出色。redis中的跳跃表也就是基本的跳跃表实现, 涉及数据结构如下:
//表节点
typedef struct zskiplistNode {
//层数组,就相当于索引
//其中包括了当前节点在每一层的前进指针以及到下一个节点的跨度
struct zskiplistLevel {
//前进指针,本层中的下一个节点
struct zskiplistNode *forward;
//跨度,用于计算节点的排位,查找节点过程中,将沿途各层的跨度累加起来,
//就是当前节点在整个跳跃表中的排位
unsigned int span;
} level[];
//后退指针,指向前一个节点
struct zskiplistNode *backward;
//分值,所有节点按照分值排序
double score;
//成员对象
robj *obj;
} zskiplistNode;
typedef struct zskiplist {
//表头节点和表尾节点
struct zskiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//最大层数
int level;
} zskiplist;
整数集合
实际上就是有序无重复的数组, 可以通过二分法进行查找操作, 不再详叙。
压缩列表
压缩列表是Redis为了节约内存而开发的, 是由一系列特殊编码的连续内存块组成的顺序型数据结构,每个压缩列表节点可以保存一个字节数组或一个整数值。其由三部分构成,如下所示:
----------------------------------------
| prev_entry_length | encoding | content |
-----------------------------------------
其中content为实际数据,encoding存储了content的具体数据类型,prev_entry_length则保存了前一个节点的占用的内存长度,通过这个值就可以定位到前一个节点。在压缩列表中为了节省内存可能会造成连锁更新,因此存在一定的性能隐患,但是由于本身出现的概率就比较小,在节点数量不多的情况下这种影响可以忽略不计。
数据类型
String类型
String类型在Redis底层可以是int,raw和embstr,如果一个String对象保存的是整数值,并且可以使用long来表示,那么将会以整数形式保存。如果一个String对象保存的是字符串值,并且其长度大于32字节,那么就直接使用SDS结构来保存,其encoding标记为raw。如果一个String对象保存的字符串长度小于32字节,那么会使用embstr编码,embstr是一种短字符串的优化,其存储还是使用SDS结构,但raw编码会调用两次内存分配函数来分别创建redisObject结构和SDS结构,而embstr编码则通过调用一次内存分配函数来分配 一块连续的空间,空间中依次包含redisObject和SDS结构。
List类型
List类型在Redis底层可以是ziplist(压缩列表)或linkedlist(双向列表)。
Hash类型
Hash类型在Redis底层可以是ziplist(压缩列表)或hashtable(Hash表),ziplist编码的哈希对象每当 有新的键值对要插入时,会将保存了键和值的节点依次推入到压缩列表表尾,因此同一键值对顺序存放在一起。
Set类型
Set类型在Redis底层可以是intset(整数集合)或hashtable(Hash表),只有当Set中的所有元素均为整数类型时才会使用intset。
Zset类型
Zset类型在Redis底层可以是ziplist(压缩列表)或skiplist(跳跃表)。
网友评论