五大基本数据结构
redis存储的都是 key:value格式的数据,key为字符串格式,value有5种
- String:字符串:sds动态字符串
- hash:map格式:数组+链表
- list:linklist(允许重复):3.2之前是ziplist linkdelist,之后是quicklist
- set:集合类型(不允许重复:intset或hashtable
- sortedset:有序集合:zset或ziplist。zset使用字典+跳表实现
String
底层是通过SDS简单动态字符串实现的
struct sdshdr {
// 用于记录buf数组中使用的字节的数目
// 和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
};

注:
free属性的值为0,表示这个SDS没有分配任何未使用空间。
len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
buf属性是一个char类型的数组,数组最后保存了空字符‘\0’。
SDS除了用来保存数据库中的字符串之外,SDS还被用作缓冲区(buffer),
如AOF模块中的AOF缓冲区,
以及客户端状态中的输入缓冲区
好处:
-
获取字符串长度快。
- 复杂度为1,而c语言中的字符串是要遍历获取长度的,复杂度为n
-
防止缓冲区溢出。
- c语言中,需要手动释放内存,容易内存溢出。而当使用sds进行修改字符串的时候,会先检测,如果不够则拓展空间。之后再进行操作。每次操作之后,len和free的值会做相应的修改。
-
减少修改字符串时带来的内存重分配次数
-
SDS通过空间预分配和惰性空间释放两种优化策略来减少内存重分配次数。
-
空间预分配
Redis通过额外分配未使用的空间,优化了SDS的字符串增长操作,减少了连续执行字符串增长操作所需的内存分配次数。惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作。当SDS缩短时,程序并不会立即回收缩短后多出来的空间,而是使用free属性将这些字节的数量记录起来,等待将来使用
注:如果需要真正地释放SDS的未使用空间,我们可以使用相应的API。
-
-
二进制安全
-
C字符串除了末尾之外不能出现空字符,否则会被程序认为是字符串的结尾。这就使得C字符串只能存储文本数据,而不能保存图像,音频等二进制数据。
-
使用SDS就不需要依赖控制符,而是用len来指定存储数据的大小,所有的SDS API都会以处理二进制的方式来处理SDS的buf的数据。程序不会对buf的数据做任何限制、过滤或假设,数据写入的时候是什么,读取的时候依然不变。
-
-
兼容部分C字符串函数
- SDS的buf的定义(字符串末尾为’\0’)和C字符串完全相同,因此很多的C字符串的操作都是适用于SDS->buf的。比如当buf里面存的是文本字符串的时候,大多数通过调用C语言的函数就可以。
Hash
redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构.当发生 hash 碰撞时将会把元素追加到链表上.值得注意的是在 Redis 的 Hash 中 value 只能是字符串.
在 Java 中 HashMap 扩容是个很耗时的操作,需要去申请新的数组,为了追求高性能,Redis 采用了 渐进式 rehash 策略.这也是 hash 中最重要的部分。
在扩容的时候 rehash 策略会保留新旧两个 hashtable 结构,查询时也会同时查询两个 hashtable.Redis会将旧 hashtable 中的内容一点一点的迁移到新的 hashtable 中,当迁移完成时,就会用新的 hashtable 取代之前的.当 hashtable 移除了最后一个元素之后,这个数据结构将会被删除.如图所示:

List
在版本3.2之前,Redis 列表list使用两种数据结构作为底层实现:
- 压缩列表ziplist
- 双向链表linkedlist
因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。
list-max-ziplist-value 64
list-max-ziplist-entries 512
默认情况下当压缩列表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码;
但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。
Set
底层使用了intset和hashtable两种数据结构存储的,intset我们可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。
set的底层存储intset和hashtable是存在编码转换的,使用intset存储必须满足下面两个条件,否则使用hashtable,条件如下:
- 结合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个
SortedSet
sortedset的一些命令:
存储:zadd key score value
获取:zrange key start end
获取:同时获取分数:zrange key start end with score
删除:zrem key value
存储的时候我们可以发现,是有一个score(分数)的,这个就是用来排序的字段。
下面我们看源码:
首先,我们来看几个参数:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
在理解这两个参数前,我们先简单了解下 ziplist 这种数据结构,顾名思义:压缩列表,怎么个压缩法,简单来说就是对于大的数据会用比较多点字节来存储,对于小的数据就用比较少的字节来存储。
if (
field-value对的数量 > ziplist.entries.size ||
任意一个filed或value长度 > zset-max-ziplist-value
) {
// 使用 zset 进行存储
} else {
// 使用 ziplist 进行存储
也就是说,redis会根据配置,选用zset,或ziplist进行存储。
而zset就是使用字典+跳表实现的:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset
zset 结构体里有两个元素,一个是 dict,用来维护 数据 到 分数 的关系,一个是 zskiplist,用来维护 分数所在链表 的关系
dict 里通过维护 哈希表 存储了 张三=>100,李四=>90 的分数关系
跳表:

跳表的插入过程:

插入之前也要先经历一个类似的查找过程,在确定插入位置后,再完成插入操作。
普通跳表有很多限制和问题,如下:
(1)分数不允许重复
(2)第一层链表为单向链表,不够灵活
于是,Redis 改良版的 zskiplist 被发明了

允许重复分数;第一层链表改为了双线链表,方便进行倒序范围查找;增加span(跨度)概念,方便计算正向和反向排名
底层数据结构
ZipList
压缩列表 ziplist 是为 Redis 节约内存而开发的。
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点 (entry),每个节点可以保存 一个字节数组 或者 一个整数值 。

1、zl bytes:用于记录整个压缩列表占用的内存字节数
2、zl tail:记录要列表尾节点距离压缩列表的起始地址有多少字节
3、zl len:记录了压缩列表包含的节点数量。
4、entryX:要说列表包含的各个节点
5、zl end:用于标记压缩列表的末端
压缩列表是一种为了节约内存而开发的顺序型数据结构
压缩列表被用作列表键和哈希键的底层实现之一
压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
添加新节点到压缩列表,可能会引发连锁更新操作。
LinkedList
就是个双向链表
typedef struct listNode{
struct listNode *prev;
struct listNode * next;
void * value;
}
typedef struct list{
//表头节点
listNode * head;
//表尾节点
listNode * tail;
//链表长度
unsigned long len;
//节点值复制函数
void *(*dup) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match)(void *ptr, void *key);
}
QuickList
考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。
后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
quickList 是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。


// 快速列表
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
...
}
// 快速列表节点
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
...
}
typedef struct quicklistNode {
struct quicklistNode *prev; //上一个node节点
struct quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct ziplist {
...
}
相对于链表它压缩了内存。进一步的提高了效率。
字典
Redis定义了dictEntry(哈希表结点),dictType(字典类型函数),dictht(哈希表)和dict(字典)四个结构体来实现字典结构,下面来分别介绍这四个结构体。
//哈希表的table指向的数组存放这dictEntry类型的地址。定义在dict.h/dictEntryt中
typedef struct dictEntry {//字典的节点
void *key;
union {//使用的联合体
void *val;
uint64_t u64;//这两个参数很有用
int64_t s64;
} v;
struct dictEntry *next;//指向下一个hash节点,用来解决hash键冲突(collision)
} dictEntry;
//dictType类型保存着 操作字典不同类型key和value的方法 的指针
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); //计算hash值的函数
void *(*keyDup)(void *privdata, const void *key); //复制key的函数
void *(*valDup)(void *privdata, const void *obj); //复制value的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2); //比较key的函数
void (*keyDestructor)(void *privdata, void *key); //销毁key的析构函数
void (*valDestructor)(void *privdata, void *obj); //销毁val的析构函数
} dictType;
//redis中哈希表定义dict.h/dictht
typedef struct dictht { //哈希表
dictEntry **table; //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
unsigned long size; //哈希表table的大小,初始化大小为4
unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
unsigned long used; //记录哈希表已有的节点(键值对)数量。
} dictht;
//字典结构定义在dict.h/dict
typedef struct dict {
dictType *type; //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。
void *privdata; //私有数据,保存着dictType结构中函数的参数。
dictht ht[2]; //两张哈希表。
long rehashidx; //rehash的标记,rehashidx==-1,表示没在进行rehash
int iterators; //正在迭代的迭代器数量
} dict;
本文详细介绍了Redis的五大基本数据结构,包括String、Hash、List、Set、SortedSet,阐述了其底层实现及特点。如String基于SDS,Hash采用渐进式rehash策略。还介绍了ZipList、LinkedList等底层数据结构,分析了它们的优缺点及适用场景。

4769

被折叠的 条评论
为什么被折叠?



