1.1 参考内容
-
参考 《说透Redis7》-- 掘金小册
1.2 redis 对象
redis 中所有数据类型都是使用 RedisObject 对象形式来表示,Redis 的每一个 value 就是一个 RedisObject ,RedisObject 主要包含三个字段(还有其他字段)
RedisObjecttype: 值对象的数据类型,常用的有5种,实际还有BitMap(2.2 版新增),HyperLogLog(2.8 版新增),GEO(3.2 版新增)、Stream(5.0 版新增), 可以通过type key来查看具体的类型StringListHashSetZset
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 数据结构