1.1 参考内容
-
参考 《说透Redis7》-- 掘金小册
1.2 redis 对象
redis
中所有数据类型都是使用 RedisObject
对象形式来表示,Redis
的每一个 value
就是一个 RedisObject
,RedisObject
主要包含三个字段(还有其他字段)
RedisObject
type
: 值对象的数据类型,常用的有5种,实际还有BitMap
(2.2 版新增),HyperLogLog
(2.8 版新增),GEO
(3.2 版新增)、Stream
(5.0 版新增), 可以通过type key
来查看具体的类型String
List
Hash
Set
Zset
encoding
: 值对象的底层编码类型*ptr
: 指向真正的底层数据结构的指针
1.2.1 为什么一个对象需要包含 type
和 encoding
- 因为
type
只是记录对象的类型 - 每一个
type
可以使用不同的底层数据结构来实现,所以还需要具体的encoding
来指明这个type
的实现是什么
1.3 数据类型和底层数据结构的对应关系
- 类型 前面都缺省
REDIS_
, 即应是REDIS_STRING
- 编码 前面都缺省
REDIS_ENCODING_
,即应是REDIS_ENCODING_INT
类型 | 编码 | 编码对应的底层数据结构 | 版本 | 条件 |
---|---|---|---|---|
STRING | INT | 整数数值实现的字符串对象 | 字符串是数值类型并且可以用long表示 | |
STRING | EMBSTR | embstr编码的简单动态字符串 | [[Redis底层数据结构#1.3.1.1 embstr和raw的区别]] | |
STRING | RAW | 简单动态字符串 | [[Redis底层数据结构#1.3.1.1 embstr和raw的区别]] | |
LIST | ZIPLIST | 压缩列表实现的列表对象 | <3.2 | 列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置) 并且每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置) |
LIST | LINKEDLIST | 双向链表实现的列表对象 | <3.2 | 不满足上面两个条件就会使用 双向链表作为 list 的底层数据结构 |
LIST | QUICKLIST | 快速链表 | >=3.2 | redis3.2 之后,List类型底层数据结构只有quicklist 一种 |
HASH | ZIPLIST | 压缩列表使用的哈希对象 | redis7中压缩列表被删除,使用listpack | 哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构 |
HASH | HT | 字典实现的哈希对象 | 不满足上述条件时使用 哈希表 作为哈希类型底层数据结构 | |
SET | INTSET | 整数集合实现的集合对象 | 集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries 配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构 |
|
SET | HT | 字典实现的集合对象 | 不满足上述条件就使用 哈希表作为 set 底层数据结构 | |
ZSET | ZIPLIST | 压缩列表实现的有序集合对象 | redis7中压缩列表被删除,使用listpack | 有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构 |
ZSET | SKIPLIST | 使用跳跃表和字典实现的有序集合对象 | 不满足上述条件时使用 哈希表 作为zset类型底层数据结构 |
1.3.1.1 embstr和raw的区别
详情可参考 Redis 常见数据类型和应用场景 下面简单总结:
- 字符串长度小于某个值时会使用
embstr
,长度高于某个值时会使用raw
- 长度边界在不同
redis
版本中不同- redis 2.+ 是 32 字节
- redis 3.0-4.0 是 39 字节
- redis 5.0 是 44 字节
embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject
和SDS
,而raw
编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject
和SDS
1.3.1.1.1 区分embstr和raw的优点
embstr
编码将创建字符串对象所需的内存分配次数从raw
编码的两次降低为一次;- 释放
embstr
编码的字符串对象同样只需要调用一次内存释放函数; - 因为
embstr
编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU
缓存提升性能。
1.4 基本数据类型对应的底层实现
1.4.1 String
String
类型的底层的数据结构实现主要是int
和SDS
(简单动态字符串)- 如果存储的是数字,则编码使用
int
类型
- 如果存储的是数字,则编码使用
sds
的实际编码又包含raw
和embstr
- 字符串长度小于 32位时使用
embstr
(embstr
的空间挨着RedisObject,RedisObject
和embstr
的内存是一次性分配的) - 字符串长度大于 32位时使用
raw
编码(RedisObject
的内存分区和raw
编码类型的空间分配是分开的,也就是需要两次内存分配)
- 字符串长度小于 32位时使用
1.4.1.1 SDS 和 C 原生字符串的区别
sds
可以保存文本和二进制数据,因为sds
有length
字段来判断字符串是否结束,c 使用 '\0' 作为结束符sds
的api
安全,不会出现缓冲区溢出,因为sds
拼接字符串前有做容量检查
1.4.2 ziplist
注:redis7中已经不再使用 ziplist,而是使用 listpack 替换 ziplist
分成了队头 、队尾 、数据 ,数据则是存放在 entry
里面的
zlbytes
存放了int
值,占 4 字节,表示整个ziplist
占的总字节数zltail
存放了int
值,占 4 字节,记录了最后一个entry
在ziplist
里面的偏移字节数,这样主要是为了方便 逆序遍历- 当知道
ziplist
的首地址,就可以结合zltail
值,计算出最后一个entry
的地址 - 每一个
entry
都会记录前一个entry
的长度,因此可以找到前一个entry
的地址,于是一个个entry
反着找,就能实现ziplist
的逆序遍历了
- 当知道
zllen
是一个 2 字节的整数,记录了整个ziplist
中的entry
个数,即元素个数zlend
占 1 个字节,值一直是 255,用来标识ziplist
结束
1.4.2.1 entry 的结构
prevlen
记录了前一个entry
节点占了多少个字节len
记录了当前这个entry
节点里面data
部分的长度data
用来存放具体的数据
1.4.3 listpack
注:listpack 是在 redis5 就引入了,在 redis7 中完全替换了 ziplist
backlen
存储的是当前element
的长度
1.4.3.1 listpack 和 ziplist 的区别
看了 ziplist
和 listpack
的整体结构,发现他俩整体没啥区别,但是 ziplist
的 entry
中的 prevlen
记录的是 前 一个 entry
的长度, listpack
的 element
中的 backlen
记录的是当前 element
的长度,主要区别就是在这里了,那么这么做的好处是什么?其实这就是用到了封装的思想了,现在 backlen
记录的是当前 element
的长度,这样每次有 element
变化时只需要操作当前 element
这个结构就好了,不会有连锁更新的问题
1.4.3.2 什么是连锁更新问题
在 ziplist
中由于当前节点的 previous_entry_length
取值决定于前一个节点的长度,所以前一个节点改动时,当前节点的 previous_entry_length
也可能会发生改变,如果连续发生这样的事情,将会触发连锁更新问题,消耗性能
1.4.4 quicklist
注:下面是 Redis7 版本中的 quicklist 结构,和 redis7 之前的区别是,redis7之前 entry 使用的是 ziplist
quicklist
同时使用双向链表结构和 listpack
连续内存空间是为了达到空间和时间上的折中
1.4.5 dict
- 哈希表对应的就是
dict
结构,如下图所示 rehashidx
为 0 时表示进行rehash
,为 -1 时表示rehash
已经完成
1.4.5.1 rehash
dict
中dictht
是一个数组,一共有两个元素,目的就是为了rehash
的时候使用
1.4.5.1.1 什么时候会触发 rehash
- 扩容
- 服务器未在执行
BGSAVE/BGREWRITEAOF
命令且哈希表的负载因子大于或等于1 - 服务端正在执行
BGSAVE/BGREWRITEAOF
命令且哈希表的负载因子大于或等于5
- 服务器未在执行
- 缩容
- 当哈希表的负载因子小于0.1时,
redis
会自动开始对哈希表进行缩容操作
- 当哈希表的负载因子小于0.1时,
1.4.5.1.2 渐进式rehash的过程
当进行 rehash
时,如果一次性完成,在数据量大的时候会阻塞主线程,因此不会一次性完成,而是分多次完成的,也就是 渐进式 rehash
- 给
ht[1]
分配空间 - 将
rehashidx
修改为0,表示rehash
操作开始执行 - 在
rehash
时若对字典进行增删改查操作,则会出现下面情况insert
:直接将键值对插入到 ht[1] 上,保证 ht[0] 的结点不会增加;delete
:同时在 ht[0] 和 ht[1] 两个哈希表上执行,避免漏删;update
:同时在 ht[0] 和 ht[1] 两个哈希表上执行,避免漏改;select
:先从 ht[0] 查,查不到的话再去 ht[1] 查;
- 在执上述操作的时候,会将 ht[0] 中对应索引位置上的所有键值对
rehash
到 ht[1] - 等到 ht[0] 上的所有键值对都
rehash
到 ht[1]上 之后,将rehashidx
修改为-1,表示rehash
过程结束
1.4.6 skiplist
-
Redis
中跳跃表使用zskiplist
表示对于跳跃表的结构可以参考 动图带你深入了解------跳跃列表(SkipList) - 掘金 (juejin.cn) Redis 数据结构