概述
数据类型实际描述的是 value 的类型,key 都是 string,常见数据类型(value)有
-
string(embstr、raw、int)
-
list(quicklist,由多个 ziplist 双向链表组成)
-
hash(ziplist、hashtable)
-
set(intset、hashtable)
-
sorted set(ziplist、skiplist)
-
bitmap
-
hyperloglog
每一种类型都用 redisObject 结构体来表示,每种类型根据情况不同,有不同的编码 encoding(即底层数据结构)
1、String
-
如果字符串保存的是整数值,则底层编码为 int,实际使用 long 来存储
-
如果字符串保存的是非整数值(浮点数字或其它字符)又分两种情况
-
长度 <= 39 字节,使用 embstr 编码来保存,即将 redisObject 和 sdshdr 结构体保存在一起,分配内存只需一次
-
长度 > 39 字节,使用 raw 编码来保存,即 redisObject 结构体分配一次内存,sdshdr 结构体分配一次内存,用指针相连
-
-
sdshdr 称为简单动态字符串,实现上有点类似于 java 中的 StringBuilder,有如下特性
-
单独存储字符长度,相比 char* 获取长度效率高(char* 是 C 语言原生字符串表示)
-
支持动态扩容,方便字符串拼接操作
-
预留空间,减少内存分配、释放次数(< 1M 时容量是字符串实际长度 2 倍,>= 1M 时容量是原有容量 + 1M)
-
二进制安全,例如传统 char* 以 \0 作为结束字符,这样就不能保存视频、图片等二进制数据,而 sds 以长度来进行读取
-
2、List
-
3.2 开始,Redis 采用 quicklist 作为其编码方式,它是一个双向链表,节点元素是 ziplist
-
由于是链表,内存上不连续
-
操作头尾效率高,时间复杂度 O(1)
-
链表中 ziplist 的大小和元素个数都可以设置,其中大小默认 8kb
-
-
ziplist 用一块连续的内存存储数据,设计目标是让数据存储更紧凑,减少碎片开销,节约内存,它的结构如下
-
zlbytes – 记录整个 ziplist 占用字节数
-
zltail-offset – 记录尾节点偏移量
-
zllength – 记录节点数量
-
entry – 节点,1 ~ N 个,每个 entry 记录了前一 entry 长度,本 entry 的编码、长度、实际数据,为了节省内存,根据实际数据长度不同,用于记录长度的字节数也不同,例如前一 entry 长度是 253 时,需要用 1 个字节,但超过了 253,需要用 5 个字节
-
zlend – 结束标记
-
-
ziplist 适合存储少量元素,否则查询效率不高,并且长度可变的设计会带来连锁更新问题
3、Hash
-
在数据量较小时,采用 ziplist 作为其编码,当键或值长度过大(64)或个数过多(512)时,转为 hashtable 编码
-
hashtable 编码
-
hash 函数,Redis 5.0 采用了 SipHash 算法
-
采用拉链法解决 key 冲突
-
rehash 时机
① 当元素数 < 1 * 桶个数时,不扩容
② 当元素数 > 5 * 桶个数时,一定扩容
③ 当 1 * 桶个数 <= 元素数 <= 5 * 桶个数时,如果此时没有进行 AOF 或 RDB 操作时
④ 当元素数 < 桶个数 / 10 时,缩容
-
rehash 要点
① 每个字典有两个哈希表,桶个数为 $2^n$,平时使用 ht[0],ht[1] 开始为 null,扩容时新数组大小为元素个数 * 2
② 渐进式 rehash,即不是一次将所有桶都迁移过去,每次对这张表 CRUD 仅迁移一个桶
③ active rehash,server 的主循环中,每 100 ms 里留出 1s 进行主动迁移
④ rehash 过程中,新增操作 ht[1] ,其它操作先操作 ht[0],若没有,再操作 ht[1]
⑤ redis 所有 CRUD 都是单线程,因此 rehash 一定是线程安全的
-
4、Sorted Set
(1)在数据量较小时,采用 ziplist 作为其编码,按 score 有序,当键或值长度过大(64)或个数过多(128)时,转为 skiplist + hashtable 编码,同时采用的理由是
-
只用 hashtable,CRUD 是 O(1),但要执行有序操作,需要排序,带来额外时间空间复杂度
-
只用 skiplist,虽然范围操作优点保留,但时间复杂度上升
-
虽然同时采用了两种结构,但由于采用了指针,元素并不会占用双份内存
(2)skiplist 要点:多层链表、排序规则、 backward、level(span,forward)
-
score 存储分数、member 存储数据、按 score 排序,如果 score 相同再按 member 排序
-
backward 存储上一个节点指针
-
每个节点中会存储层级信息(level),同一个节点可能会有多层,每个 level 有属性:
-
foward 同层中下一个节点指针
-
span 跨度,用于计算排名,不是所有跳表都实现了跨度,Redis 实现特有
-
(3)多层链表可以加速查询,规则为,从顶层开始
- 大于同层右边的,继续在同层向右找
- 相等找到了
- 小于同层右边的或右边为 NULL,下一层,重复 1、2 步骤
-
以查找【崔八】为例
-
从顶层(4)层向右找到【王五】节点,22 > 7 继续向右找,但右侧是 NULL,下一层
-
在【王五】节点的第 3 层向右找到【孙二】节点,22 < 37,下一层
-
在【王五】节点的第 2 层向右找到【赵六】节点,22 > 19,继续向右找到【孙二】节点,22 < 37,下一层
-
在【赵六】节点的第 1 层向右找到【崔八】节点,22 = 22,返回
-
注意
数据量较小时,不能体现跳表的性能提升,跳表查询的时间复杂度是 $log_2(N)$,与二叉树性能相当