Redis自己构建了一种SDS(Simple dynamic string)的数据结构,用于存储字符串。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字符数组,用于保存字符串
char buf[];
}
redis> SET msg "hello world"
OK
redis> APPEND msg " again!"
(integer) 18
redis> GET msg
"hello world again!"
首先, SET
命令创建并保存 hello world
到一个 sdshdr
中,这个 sdshdr
的值如下:
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0";
}
当执行 APPEND命令时,相应的 sdshdr
被更新,字符串 " again!"
会被追加到原来的 "hello world"
之后:
struct sdshdr {
len = 18;
free = 18;
buf = "hello world again!\0 "; // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
}
而在执行APPEND之后, Redis 为 buf
创建了多于所需空间一倍的大小。以下为伪代码
def sdsMakeRoomFor(sdshdr, required_len):
# 预分配空间足够,无须再进行空间分配
if (sdshdr.free >= required_len):
return sdshdr
# 计算新字符串的总长度
newlen = sdshdr.len + required_len
# 如果新字符串的总长度小于 SDS_MAX_PREALLOC
# 那么为字符串分配 2 倍于所需长度的空间
# 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
# SDS_MAX_PREALLOC 的值为 1024 * 1024,即1MB
if newlen < SDS_MAX_PREALLOC:
newlen *= 2
else:
newlen += SDS_MAX_PREALLOC
# 分配内存
newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
# 更新 free 属性
newsh.free = newlen - sdshdr.len
# 返回
return newsh
当满足以下两个条件时,使用压缩列表ziplist实现
里面几个字段的意思分别是:
节点字段entry结构如下
优势:有效地节省内存开销
缺点:1、查找复杂度高
2、连锁更新:压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
当不满足上述两个条件时,列表使用linkedlist实现
linkedlist是一个双向链表,每个entry包含向前向后的指针。插入和删除效率很高
quicklist(Redis 3.2 引入)
一个由ziplist组成的双向链表。但是一个quicklist可以有多个quicklist节点,它很像B树的存储方式。是在redis3.2版本中新加的数据结构,用在列表的底层实现。
哈希底层有两种实现方式,一种是上述的压缩列表的方式,另一种是使用字典(哈希表)实现。
当同时满足一下两个条件时使用压缩列表,这也是因为压缩列表只适合节点数量不多的实现场景
字典的实现涉及三部分结构体:字典、哈希表、哈希表节点。其中哈希表节点保存一个键值对。
其中,dict表示字典,dictht表示哈希表,dictEntry表示哈希节点。
dicEntry是一个数组,和JDK中HashMap实现差不多,使用的是拉链法。
dictht有两个,这是为了实现扩展和收缩哈希表使用的,通过rehash方法实现。
rehash步骤:
触发扩容操作的条件:
触发收缩操作的条件:
在进行rehash过程中,dict添加、查询和删除操作:
添加:添加在ht[1]中
查询:首先会在ht[0]中查询,查询不到则去ht[1]中查询
删除:ht[0]和ht[1]都要查找并删除
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不时,就会使用整数集这个数据结构作为底层实现。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
可以看到,保存元素的容器是一个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。比如:
升级操作:就是当我们将一个新元素加入到整数集合里面,该元素的类型比整数集合现有的所有元素的类型都要长时,按新元素的类型扩展 contents 数组的空间大小。但不支持降级操作
底层使用压缩列表和跳表实现。使用压缩列表的条件是:元素数量不超过128个并且元素长度小于64字节。
跳表实现涉及两个结构体:zskiplist和zskiplistNode。跳表优先在顶层搜索,是一种二分查找算法。
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;