redis内部有 简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表六种数据结构。
SDS的定义:
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
和C语言中的字符串相比,SDS有以下特性:
常数复杂度获取字符串长度: c字符串不记录自身长度,需要获取时需要遍历字符串,操作复杂度为O(n); SDS直接通过len属性获取长度,复杂度仅为O(1);
杜绝缓冲区溢出: c字符串执行字符串拼接操作时需要预先分配内存,若未分配内存造成容易造成缓冲区溢出; 在执行字符串缩减操作时,需要手动释放内存,否则造成内存泄漏。 SDS的len属性,避免了缓冲区的溢出问题;free属性避免了内存泄漏的问题;
减少修改字符串时带来的内存重分配次数: C字符串执行拼接或截断操作时为了避免缓冲区溢出和内存泄漏问题, 需要进行内存重分配;SDS通过len属性结合free属性实现了空间预分配和惰性空间释放两种策略来减少了内存重分配次数;
当字符串长度变长需要扩展空间时,SDS不仅会分配字符串需要的空间,还会分配额外的未使用空间; a.若SDS的长度小于1MB, 分配的额外空间为当前长度;例如扩展后的len长度为13字节,则额外分配的空 间为13字节; b. 若SDS的长度大于1MB,分配的1MB的额外空间;例如当前len长度为10MB,则额外分配的空间为 1MB, 空间预分配后总大小为10MB+1MB+1bytes;
当执行字符串截断时,释放的空间会加到free中,不会立即释放;减少之后的再分配;
二进制安全: C字符串必须符合某种编码,如ASCALL; reids使用buf保存字节数组,可以保存任何格式的二进制数据;
节点的结构
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;
链表是双端列表,包含表头指针,表尾指针,链表长度。
字典使用哈希表实现,哈希表结构定义如下:
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
哈希表节点结构如下:
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
跳跃表节点结构:
typedef struct zskiplistNode {
//层
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode;
层:一个节点有多个level, 一个level中包括前进指针、跨度;每次创建节点时,根据幂次定律随机生成一个1-32的数作为level的高度,即level数组的长度; 前进指针:指向下一个节点;每一层的前进指针指向不同; 跨度:用于记录当前节点与下个节点的距离; 分值和成员:跳跃表中的所有节点按照分值从小到大排序;成员对象指向一个SDS值;
跳跃表结构: 跳跃表由多个跳跃表节点组成,包括头结点、尾节点、数量、最大层数;
typedef struct zskiplist {
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
} zskiplist;
整数集合是集合键的底层实现之一,当集合中只包含整数,且数量不多时,会使用整数集合来实现; 结构如下:
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
集合中的每一项在数组中按从小到大的顺序排列,且不重复;
压缩列表是列表键和哈希键的底层实现之一,当列表中只包含少量列表项且每个项是小的整数或者小的字符串时,reids会用压缩列表来实现列表键和哈希键;
每个压缩列表的节点可以保存一个字节数组或一个整数;字节数组有为三种长度;
压缩列表存在连锁更新的问题,由于内部是连续的内存块组成的顺序型存储结构,当某个节点需要扩展字节长度时,后续节点的previous_entry_length需要扩展大小,因此会引发连续更新;但是因为节点数量不多,因此性能影响可以忽略;
redis并没有使用以上六种数据结构实现键值对数据库,而是基于这些数据结构创建了对象,包括字符串对象,列表对象、哈希对象,集合,有序集合这五种类型的对象;
redis对象的结构如下:
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
由类型、编码和底层数据结构指针组成;类型与对象对应关系如下:
image.png
对象与编码的关系如下:
image.png
字符串对象: 字符串对象的编码可以是int, embstr, raw;
列表对象: 列表对象的编码可以是ziplist,linkedlist; 当列表对象保存的字符串长度小于64字节,且数量小于512,则使用ziplist,否则使用linkedlist;
哈希对象: 哈希对象使用ziplist的条件与列表对象相同;
集合对象: 集合对象使用整数集合或字典实现;当集合中元素都是整数且数量小于512时,使用整数结合实现;
有序集合: 有序集合使用过压缩列表或跳跃表和字典实现;当集合内元素数量小于128且元素大小小于64字节,则使用压缩列表;否则使用跳跃表和字典实现;