Redis底层数据结构

1.1 参考内容

  1. Redis 常见数据类型和应用场景

  2. 参考 《说透Redis7》-- 掘金小册

1.2 redis 对象

redis 中所有数据类型都是使用 RedisObject 对象形式来表示,Redis 的每一个 value 就是一个 RedisObjectRedisObject 主要包含三个字段(还有其他字段)

  • 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 为什么一个对象需要包含 typeencoding

  1. 因为 type 只是记录对象的类型
  2. 每一个 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会通过一次内存分配函数来分配一块连续的内存空间来保存redisObjectSDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObjectSDS
1.3.1.1.1 区分embstr和raw的优点
  • embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;
  • 释放 embstr 编码的字符串对象同样只需要调用一次内存释放函数;
  • 因为embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。

1.4 基本数据类型对应的底层实现

1.4.1 String

  1. String 类型的底层的数据结构实现主要是 intSDS(简单动态字符串)
    1. 如果存储的是数字,则编码使用 int 类型
  2. sds 的实际编码又包含 rawembstr
    1. 字符串长度小于 32位时使用 embstr(embstr 的空间挨着 RedisObject,RedisObjectembstr 的内存是一次性分配的)
    2. 字符串长度大于 32位时使用 raw 编码(RedisObject 的内存分区和 raw 编码类型的空间分配是分开的,也就是需要两次内存分配)

1.4.1.1 SDS 和 C 原生字符串的区别

  1. sds 可以保存文本和二进制数据,因为 sdslength 字段来判断字符串是否结束,c 使用 '\0' 作为结束符
  2. sdsapi 安全,不会出现缓冲区溢出,因为 sds 拼接字符串前有做容量检查

1.4.2 ziplist

注:redis7中已经不再使用 ziplist,而是使用 listpack 替换 ziplist 分成了队头队尾数据 ,数据则是存放在 entry 里面的

  • zlbytes 存放了 int 值,占 4 字节,表示整个 ziplist 占的总字节数
  • zltail 存放了 int 值,占 4 字节,记录了最后一个 entryziplist 里面的偏移字节数,这样主要是为了方便 逆序遍历
    • 当知道 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 的区别

看了 ziplistlistpack 的整体结构,发现他俩整体没啥区别,但是 ziplistentry 中的 prevlen 记录的是 一个 entry 的长度, listpackelement 中的 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

  • dictdictht 是一个数组,一共有两个元素,目的就是为了 rehash 的时候使用
1.4.5.1.1 什么时候会触发 rehash
  • 扩容
    • 服务器未在执行 BGSAVE/BGREWRITEAOF 命令且哈希表的负载因子大于或等于1
    • 服务端正在执行 BGSAVE/BGREWRITEAOF 命令且哈希表的负载因子大于或等于5
  • 缩容
    • 当哈希表的负载因子小于0.1时,redis 会自动开始对哈希表进行缩容操作
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

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
2401_857610036 小时前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
sam-1237 小时前
k8s上部署redis高可用集群
redis·docker·k8s
看山还是山,看水还是。7 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
谷新龙0017 小时前
Redis运行时的10大重要指标
数据库·redis·缓存
精进攻城狮@7 小时前
Redis缓存雪崩、缓存击穿、缓存穿透
数据库·redis·缓存
杨哥带你写代码8 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端