Redis存储引擎剖析:从哈希表到智能数据结构

引用

在Redis高效表现的背后,是一套精密的存储引擎设计。全局哈希表作为数据的"总目录",通过指针灵活指向各种RedisObject,实现了统一的快速访问。

更巧妙的是,Redis会根据数据特征智能选择最优存储结构:

  • Set: 在纯整数场景下采用紧凑的整数数组 ,复杂场景切换到哈希表。

  • **SDS字符串:**通过5种精确定制的头部类型,实现极致的内存效率。

  • Zset/List/Hash: 在小数据量时使用内存友好的压缩列表 ,大数据量时自动切换至性能更优的跳表哈希表。

Redis的全局哈希表

如果值是集合类型的话,作为数组元素的哈希桶怎么来保存呢?

哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

哈希桶的entry结构图,key和value都指向一个RedisObject。

Redis的数据类型

Zset底层数据结构的切换

压缩列表

  • 当一个 zset 中的元素个数小于 zset-max-ziplist-entries 配置的阈值(默认值为 128)。
  • 当 zset 中的每个元素的长度小于 zset-max-ziplist-value 配置的阈值(默认值为 64 字节)。
  • 当同时满足这些条件时,Redis 将使用压缩列表来保存 zset。

跳表

  • 如果 zset 中的元素个数超过 zset-max-ziplist-entries。
  • 如果 zset 中的任何元素长度超过 zset-max-ziplist-value。
  • 当同时满足任一条件时,Redis 将使用跳表来保存 zset。

List底层数据结构的切换

压缩列表

  • 当一个 list 中的元素个数小于 list-max-ziplist-entries 配置的阈值(默认值为 512)。
  • 当 list 中的每个元素的长度小于 list-max-ziplist-value 配置的阈值(默认值为 64 字节)。
  • 当同时满足这些条件时,Redis 将使用压缩列表来保存 list。

双向链表

  • 如果 list 中的元素个数超过 list-max-ziplist-entries。
  • 如果 list 中的任何元素长度超过 list-max-ziplist-value。
  • 当同时满足任一条件时,Redis 将使用双向链表来保存 list。

Hash底层数据结构的切换

压缩列表

  • 当一个 list 中的元素个数小于 hash-max-ziplist-entries 配置的阈值(默认值为 512)。
  • 当 list 中的每个元素的长度小于 hash-max-ziplist-value 配置的阈值(默认值为 64 字节)。
  • 当同时满足这些条件时,Redis 将使用压缩列表来保存 hash。

哈希表

  • 如果 list 中的元素个数超过 hash-max-ziplist-entries。
  • 如果 list 中的任何元素长度超过 hash-max-ziplist-value。
  • 当同时满足任一条件时,Redis 将使用双向链表来保存 hash。

Set底层数据结构的切换

整数数组

  • 当 Set 中的所有元素都是整数,且元素个数小于 set-max-intset-entries 配置的阈值(默认值为 512)。

哈希表

  • 如果 Set 中包含非整数类型的元素时。
  • 如果 Set 中的元素个数超过 set-max-intset-entries。
  • 当同时满足任一条件时,Redis 将使用哈希表来保存 Set。

Redis的底层数据结构

简单动态字符串

Redis 5.0 的 SDS 数据结构:

len:记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。

alloc:分配给字符数组的空间长度,不包括SDS头部和结尾的空字符。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。

flags:用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

buf[]:字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。

扩容机制

  • 如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍 的 newlen,并适当升级flags类型。
  • 如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB,并适当升级flags类型。

flags如何节省内存空间

sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,这5种类型的区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同,头部占用空间也不同。

头部占用空间区别:

  • sdshdr5

    struct sdshdr5 {
    unsigned char flags; // 低5位表示字符串长度,高3位表示类型
    char buf[];
    };
    占用空间:1字节(仅包含flags)。

  • sdshdr8

    struct sdshdr8 {
    uint8_t len; // 当前字符串长度
    uint8_t alloc; // 分配的空间大小
    unsigned char flags; // 类型标识
    char buf[];
    };
    占用空间:1(len) + 1(alloc) + 1(flags) = 3字节。

  • sdshdr16

    struct sdshdr16 {
    uint16_t len; // 当前字符串长度
    uint16_t alloc; // 分配的空间大小
    unsigned char flags; // 类型标识
    char buf[];
    };
    占用空间:2(len) + 2(alloc) + 1(flags) = 5字节。

  • sdshdr32

    struct sdshdr32 {
    uint32_t len; // 当前字符串长度
    uint32_t alloc; // 分配的空间大小
    unsigned char flags; // 类型标识
    char buf[];
    };
    占用空间:4(len) + 4(alloc) + 1(flags) = 9字节。

  • sdshdr64

    struct sdshdr64 {
    uint64_t len; // 当前字符串长度
    uint64_t alloc; // 分配的空间大小
    unsigned char flags; // 类型标识
    char buf[];
    };
    占用空间:8(len) + 8(alloc) + 1(flags) = 17字节。

压缩列表

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示压缩列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

跳表

跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。查找、删除、新增的时间复杂度都是log(n)。


感谢您的阅读!如果文章中有任何问题或不足之处,欢迎及时指出,您的反馈将帮助我不断改进与完善。期待与您共同探讨技术,共同进步!

相关推荐
饮长安千年月2 小时前
玄机-第八章 内存马分析-java02-shiro
数据库·安全·web安全·网络安全·应急响应
chxii3 小时前
第五章:MySQL DQL 进阶 —— 动态计算与分类(IF 与 CASE WHEN)多表查询
数据库·mysql
Mr_Xuhhh3 小时前
五种IO模型与非阻塞IO
数据库
拾零吖3 小时前
数据库 - SQL
数据库·sql
不会c嘎嘎3 小时前
MySQL -- 库的操作
数据库·mysql
陌上桑花开花3 小时前
DBeaver常用配置
数据库
百***87443 小时前
MySQL 查看有哪些表
数据库·mysql·oracle
曹牧3 小时前
Oracle:查询当前正在等待执行的SQL语句
linux·数据库·oracle
_Kafka_3 小时前
在 Oracle Data Guard 环境中,手工将备库(Standby)切换为主库(Primary)
数据库·oracle