type
status
date
slug
summary
tags
category
password
1、概述
Redis 的数据类型和底层结构之间的关系是 “抽象与实现” 的关系。Redis 对外提供高级的数据类型(如 String、List、Hash 等),但内部会根据数据的特点(如大小、数量、元素类型等)选择不同的底层结构来实现,以达到性能与内存的平衡。
Redis 共有五种基本数据类型:
- String(字符串):最基本的数据类型,可以存储文本、数字或二进制数据(最大 512MB)。
- List(列表):有序的字符串集合,基于双向链表实现,支持两端插入和删除。
- Set(集合):无序不可重复的字符串集合,支持交集、并集、差集运算。
- Hash(散列):键值对集合,键为不可重复的字符串,值为任意类型。
- Sorted Set(有序集合):又称为 Zset,类似于 Set,但每个元素关联一个 score(分数),用于排序。
另外还有一些其他扩展的数据类型:
- Bitmap(位图):2.2 版新增,类似于一个以 Bit 为单位的数组,数组的每个单元只能存储 0 和 1,用少量空间记录大量状态(状态值只能为0或者),适合二值统计
- HyperLogLog(基数统计):2.8 版新增,用于高效估算集合的基数(不重复元素的数量)。使用固定的大小(不超过12k),用来统计海量数据中不重复的元素个数。基于伯努利算法,不会存储原始基数数据,用于基数统计,误差率约 0.81%。
- GEO(地理空间):3.2 版新增,存储地理位置(经纬度),支持距离计算、范围查询。
- Stream(流):5.0 版新增,发布订阅(Pub/Sub)模式的升级版,类似于 Kafka 的消息队列,提供持久化、多消费者组等功能。
这些数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这七种数据结构:
- 简单动态字符串(SDS)
- LinkedList(双向链表)
- Dict(哈希表/字典)
- SkipList(跳跃表)
- Intset(整数集合)
- ZipList(压缩列表)
- QuickList(快速列表)
Redis 根据使用场景为各种数据类型选择最优的底层数据结构实现,实现性能和存储空间的最优解。
2、Redis的底层数据结构
底层数据结构 | 描述 | 对应数据类型 |
简单动态字符串(SDS) | 用于存储字符串值和键名。 | String |
LinkedList(双向链表) | 双端链表(3.2 版本后被 QuickList 代替)。 | List |
Intset(整数集合) | 整数值集合,用于 Set 中只包含整数元素的场景。本质上是一块连续内存空间。 | Set |
Dict(哈希表/字典) | Redis 的键值对存储核心结构。 | Hash,Set |
SkipList(跳跃表) | 用于实现有序集合(Sorted Set)。在链表基础上改进过来的,实现了一种多层的有序链表,能支持平均 O(logN) 复杂度的节点查找。 | Zset |
ZipList(压缩列表) | 用于列表(List)、哈希(Hash)和有序集合(Sorted Set)的小数据存储。为了节约内存的设计,连续内存块组成的顺序型数据结构。 | List,Hash,Zset |
QuickList(快速列表) | 3.2 版引入,QuickList 是 ZipList 和 LinkedList 的混合体,是由多个 ZipList 组成的双向链表 | List |
2.1 简单动态字符串(SDS)
C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。例如下图就是字符串
xiaolin
的 char* 字符数组的结构:
在 C 语言里,对字符串操作时,char* 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用
\0
表示,意思是指字符串的结束。因此,C 语言标准库中的字符串操作函数就通过判断字符是不是 \0
来决定要不要停止操作,如果当前字符不是 \0
,说明字符串还没结束,可以继续操作,如果当前字符是 \0
是则说明字符串结束了,就要停止操作。C 语言的字符数组的缺点是:- 获取字符串长度的时间复杂度是 O(N),对字符串的操作效率不高。
- 字符串的结尾是以
\0
字符标识,字符串里面不能包含有\0
字符,因此不能保存二进制数据。
- 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止。
Redis 中的字符串没有使用 C 语言自带的字符数组,而是自定义了
SDS
类型。这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 SDS
, 否则的话, 它就是一个 long 类型的值。
- len:已使用长度,len属性可以实现字符串长度O(1)事件的计算。
- free:未使用长度,free属性可以用于动态扩容。
- buf:实际存储字符串的数组
使用 SDS 的优点:
- O(1)时间复杂度计算字符串长度。因为有专门的 len 成员变量来记录长度。
- 二进制安全。因为 SDS 不需要用
\0
字符来标识字符串结尾了,所以可存储包含\0
的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上\0
字符。
- 支持动态扩容。使用append命令追加字符串时,假设 SDS_MAX_PREALLOC 默认大小是1M。如果新字符串的总长度小于 SDS_MAX_PREALLOC,就为字符串分配 2 倍于所需长度(新字符串的长度)的空间;否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
使用 SDS 的缺点:
- 如果记录的值长度很短,元数据的内存开销可能会比字符串本身开销还大。
2.2 LinkedList(双向链表)
c语言底层没有链表结构,redis本身实现了一个双端链表结构。

由上图可以看到,linkedlist结构分为两部分:
- list:用来持有链表,可以迅速获取头尾节点,计算节点数等。
- 在同一个跳表中,多个节点可以包含相同的分值(score),但是每个节点的成员对象(ele)的必须唯一。
- 跳表节点按照分值(score)的大小进行排序,当分值(score)相同时,节点按照成员对象(ele)的大小进行排序。
- level 上的前进指针可以跳过多个节点,但是跳表节点的后退指针只能返回前一个节点。
- head:表头指针。
- tail:表尾指针。
- len:链表长度。
- dup 函数用于复制链表节点所保存的值。
- free 函数用于释放链表节点所保存的值。
- match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。(dup 、 free 和 match 用于实现多态链表所需的类型特定函数。)
跳表的特点:
- listNode:包含具体的节点内容
linkedlist 的优点:
- list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)。
- list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
- listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值。
linkedlist 的缺点:
- 链表每个节点之间的内存都是不连续的,无法很好利用 CPU 缓存。
- 不管保存节点数多少,哪怕保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
2.3 Intset(整数集合)
整数集合(intset)是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,就会使用整数集这个数据结构作为底层实现。 整数集合本质上是一块连续内存空间。

- encoding:整数集合的实际类型。
- 如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组中每一个元素的类型都是 int16_t。
- 如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组中每一个元素的类型都是 int32_t。
- 如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组中每一个元素的类型都是 int64_t。
- length:整数集合包含的元素数量,也就是contents数组的长度
- contents:整数数组,各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但 contents 数组的真正类型取决于 encoding 属性的值。
整数集合升级过程:当向一个底层为 int16_t 数组的整数集合添加一个 int64_t 类型的整数值时, 整数集合已有的所有元素都会被转换成 int64_t 类型, 所以 contents 数组保存的四个整数值都是 int64_t 类型的。
2.4 Dict(哈希表/字典)
哈希表是一种保存键值对(key-value)的数据结构。

dict的结构由三部分组成:
- dict:dict的整体结构,底层核心是两个dictht(dict hash table)。平时只有ht[0]是有内容的,ht[1]是null。ht[1] 只会在对 ht[0] 进行 rehash 时使用。
- dictht:dictEntry数组结构,底层核心是dictEntry元素
- size 属性记录了哈希表的大小, 也即是 table 数组的大小
- used 属性则记录了哈希表目前已有节点(键值对)的数量。
- dictEntry:真正存储节点元素的结构。不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
Hash 冲突时候 rehash 过程是怎样的?
Redis 采用了「链式哈希」的方法来解决哈希冲突,但是在哈希冲突比较严重的情况下,还是会发生rehash。rehash的过程:
- 新建一个ht[1],容量一般为ht[0]的两倍以上。
- 把ht[0]的元素搬运到ht[1],最后用ht[1]替换ht[0]。
- 原来的h[0]清空元素,然后把原来的ht[0]改成ht[1]
如果ht[0]的数据量非常大,那么在迁移至ht[1]的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
- 新建一个ht[1],容量一般为ht[0]的两倍以上。
- 每次往集合find、add、delete等操作之前,先把ht[0]的第一个不为空的bucket上的所有元素搬运到ht[1],然后再进行这次命令操作。
- 随着处理客户端发起的哈希表操作请求数量越多,最终会在某个时间点将ht[0]的所有元素迁移到ht[1]。
- 然后同普通rehash操作。
触发rehash的条件:used/size 的值称为负载因子,当负载因子满足某种条件时会进行rehash:
- used/size>1时,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,会进行渐进式rehash。
- used/size>5时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,会进行强制rehash。
2.5 SkipList(跳跃表)
Skiplist 是 Redis 设计的一种非常巧妙的数据结构,通过空间换时间的方式,在链表的基础上构建多级索引,实现了近乎平衡树的查询效率,同时其实现复杂度远低于平衡树。
2.5.1 跳表的原理
为什么需要跳表?
在有序集合中,我们需要高效地支持以下几种操作:
- 插入:在保持顺序的情况下插入新元素。
- 删除:快速找到并删除指定元素。
- 单个查询:根据键值快速查找元素。
- 范围查询:获取指定区间的所有元素。
技术考虑:
- 如果使用普通的有序链表,虽然插入和删除是 O(1)(前提是找到位置),但查找效率是 O(n),这在大数据量下是不可接受的。
- 如果使用平衡树(如 AVL 树、红黑树),虽然所有操作都可以在 O(log n) 时间内完成,但实现复杂,且在并发环境下需要复杂的锁机制。
跳跃表在两者之间取得了很好的平衡:
- 平均时间复杂度为 O(log n),接近平衡树的效率,最坏情况为 O(n)。
- 实现相对简单,相比平衡树实现更简单,易于理解和调试。
- 支持高效的范围查询,因为底层仍然是链表结构。
跳表的实现原理:
想象一个有序链表:
如果要查找元素
55
,只能从头开始逐个遍历,效率是 O(N)。跳表相当于在链表的基础上加了多层稀疏索引来加速查找:
- 第 0 层: 完整的原始链表,包含所有元素。
- 第 1 层: 一个稀疏的子链表,是第 0 层链表的“索引”,比如每隔 2 个元素提取一个。
- 第 2 层: 在第 1 层的基础上,再建立一个更稀疏的索引,比如每隔 2 个元素提取一个。
- ...
- 第 N 层: 最高层,最稀疏的索引。
基于多层稀疏索引的查找过程(以查找
55
为例):- 从最高层(第 N 层)开始向右查找。
- 如果当前节点的下一个节点值
<=
目标值,则继续向右移动。
- 如果下一个节点值
>
目标值,则下降一层。
- 重复步骤 2 和 3,直到在第 0 层找到目标值或确认它不存在。
这个过程类似于二分查找,每次操作都排除掉大量不可能包含目标的节点。
2.5.2 跳表的数据结构

zkiplist 结构分为两部分:
- zskiplist(跳跃表结构):持有 zkiplist 的整体结构。虽然多个 zskiplistnode 就可以组成一个跳表,但是通过 zkiplist 可以更方便进行整体操作。
header
:指向表头节点(头节点不是一个有效节点,没有成员对象)tail
:指向表尾节点level
:记录跳跃表的最大层数(不包含头节点,因为头节点都是32层)length
:记录跳跃表的节点数(不包含头节点),使得ZLEN
命令的时间复杂度为 O(1)。
- zskiplistnode(跳跃表节点):真正保存元素的数据结构。
level[]
:zskiplist Level 结构的数组,这是实现链表层级关系的关键。level 可能包含多个元素,每一个元素对应代表跳表的一层。每次创建一个新跳表节点的时候,会随机生成一个 1 到 32 的随机数作为 level 数组的大小,这个大小也就是 level 的层高。每个 level 元素都包含两部分:forward
:前进指针,指向表尾方向的下一个跳表节点。span
:跨度,用来记录两个节点之间的距离,实际上是为了计算这个节点在跳表中的排位。计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。backward
:后退指针,后退指针只能指向相邻的前一个节点score
:分值,双精度浮点数,跳表中的元素按照 score 从小到大的顺序排序,对应 Zset 对象的元素权重。ele
:成员对象,一个 SDS 字符串,存储真正的数据。,对应 Zset 对象的元素
特点:
- 在同一个跳表中,多个节点可以包含相同的分值(score),但是每个节点的成员对象(ele)的必须唯一。
- 跳表节点按照分值(score)的大小进行排序,当分值(score)相同时,节点按照成员对象(ele)的大小进行排序。
- level 上的前进指针可以跳过多个节点,但是跳表节点的后退指针只能返回前一个节点。
2.5.3 跳表的操作过程
跳表的插入过程
我们假设要插入的值为
value
。第 1 步:初始化辅助数组
update
- 创建一个数组
update
,其长度为跳表的最大层数(MAX_LEVEL
)。
- 这个数组的作用:对于每一层
i
,update[i]
将记录在第 i 层中,最后一个小于value
的节点。换句话说,新节点在第i
层应该插入在update[i]
节点的后面。
- 这一步是为了在后续插入时,能快速找到每一层的前驱节点,而无需每次都从头部开始遍历。
第 2 步:查找每一层的前驱节点
- 从跳表的当前最高层(
currentMaxLevel
)开始,从上至下遍历每一层。
- 在每一层中,从
update[i]
的位置(初始时,update[i]
是头节点)向右遍历,直到找到该层中最后一个小于value
的节点,并用这个节点更新update[i]
。
- 通俗理解:就像在一个有很多快速通道(索引)的火车站里,从最高的快速通道开始找,快速确定“大概在哪个区域”,然后逐层下降,通道越来越慢,定位也越来越精确,最终在底层(第0层)找到最精确的插入位置,并记录下每一层快速通道的“入口点”(
update
数组)。
第 3 步:生成新节点的随机层数
- 这是跳表区别于普通链表的关键,也是它能保持平衡的核心。
- 为新节点随机生成一个层数
level
(例如,通过“抛硬币”的方式:正面则层数+1,直到抛出反面为止)。
- 通常会有最大层数限制(
MAX_LEVEL
),所以level = min(随机层数, MAX_LEVEL)
。
- 随机层数的概率:通常设计为第
i
层出现的概率是第i-1
层的 1/2。这意味着大约一半的节点出现在第1层,1/4的节点在第2层,1/8的节点在第3层,以此类推。这种概率分布保证了上层索引的稀疏性,从而保持了高效的搜索路径。
第 4 步:插入新节点并更新指针
- 如果新节点的随机层数
level
超过了跳表当前的最高层数currentMaxLevel
,那么需要: - 将
update
数组中从currentMaxLevel+1
到level
层的位置都初始化为头节点(因为在这些更高的新层中,头节点就是最后一个节点)。 - 更新
currentMaxLevel
为level
。
- 对于从第 0 层到第
level
层的每一层i
: - 将新节点的
next[i]
指针指向update[i].next[i]
(即原链表中update[i]
的后一个节点)。 - 将
update[i].next[i]
指向新节点。
- 这个过程就像是在多条链表中同时插入同一个节点:在第0层的标准链表中插入它,如果它还在第1层,就在第1层的索引链表中也插入它,以此类推。
第 5 步:更新其他信息(可选)
- 更新跳表的长度(元素个数)等元信息。
示例演示:
假设我们有一个跳表,初始状态如下(数字表示节点值,
-∞
表示头节点):现在我们要插入值
5
。- 初始化
update
数组:创建一个长度为MAX_LEVEL
(例如4)的数组。
- 查找前驱节点:
- 从第3层开始:
-∞
的 next 是NULL
(NULL
> 5? 是,所以停止),所以update[3] = -∞
。 - 下降到第2层:
-∞
的 next 是6
(6 > 5? 是,停止),所以update[2] = -∞
。 - 下降到第1层:
-∞
的 next 是3
(3 < 5, 继续) —>3
的 next 是6
(6 > 5, 停止),所以update[1] = 3
。 - 下降到第0层:
3
的 next 是4
(4 < 5, 继续) —>4
的 next 是6
(6 > 5, 停止),所以update[0] = 4
。 update
数组现在大概是:[-∞, -∞, 3, 4]
(下标从高到低)。
- 随机层数:假设“抛硬币”结果为
level = 2
(即新节点需要出现在0, 1, 2层)。
- 插入节点:
- 因为
level=2
没有超过当前最高层3
,所以不需要更新最高层。 - 对于第 0 层:新节点
5
的next[0]
指向update[0].next[0]
(即4.next[0]
是6
)。然后将4.next[0]
指向5
。 - 对于第 1 层:新节点
5
的next[1]
指向update[1].next[1]
(即3.next[1]
是6
)。然后将3.next[1]
指向5
。 - 对于第 2 层:新节点
5
的next[2]
指向update[2].next[2]
(即∞.next[2]
是6
)。然后将∞.next[2]
指向5
。 - 第3层不需要变动。
插入后的跳表变为:
跳表的查询过程
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
- 如果当前节点的分值「小于」要查找的分值时,跳表就会访问该层上的下一个节点。
- 如果当前节点的分值「等于」要查找的分值时,并且当前节点的成员对象「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
- 如果上面两个条件都不满足,或者下一个节点为NULL时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针(注意是下一层指针,比如当前是level2,跳到level1指针查找),然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
- 当level已经到最底层,当前节点的分值「小于」要查找的分值时,下一个节点为NULL或者下一个节点的分值「大于或等于」要查找的分值的时候,该位置就是要查找的位置,查找结束。
计算排名 (ZRANK)
这是
span
跨度大显身手的地方。- 在查找某个节点的过程中,从高层向低层遍历。
- 累加沿途经过的节点的
span
值。当最终找到目标节点时,这个累加值就是该节点的排名(Rank)。
- 因为查找过程是 O(log N),所以计算排名也是 O(log N)。
范围查询 (ZRANGE)
- 先用 O(log N) 的时间找到范围起始节点。
- 然后沿着第 0 层的链表(通过
forward
指针)向后遍历,直到达到范围结束条件。因为链表遍历是 O(N),但这里 N 是范围大小,而不是整个集合的大小,所以对于小范围查询非常高效。

例如图中skiplist总共包含4层索引,假如从下到上分别为level1~4,查找数字23的位置:
- 从头节点的level4开始查找,找到数字7节点,7 < 23,所以继续查找数字7节点level4的下一节点。
- 下一节点为NULL,返回数字7节点,查找数字7节点level3的下一节点。
- 下一节点为数字37节点,37>23,返回数字7节点,查找数字7节点level2的下一节点。
- 下一节点为数字19节点,19<23,所以继续查找数字19节点level2的下一节点。
- 下一节点为数字37节点,37>23,返回数字9节点,查找数字19节点level1的下一节点。
- 下一节点为数字22节点,22<23,所以继续查找数字22节点level1的下一节点。
- 下一节点为数字26节点,26>23,但level已经到最底层,所以数字23的最终位置为数字22和26之间。
跳表节点层数设置:跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
2.6 ZipList(压缩列表)
压缩列表是redis为了节约内存设计的数据结构,本质是一块连续内存的数据结构,有点类似于数组。

- zlbytes:整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
- zltail:压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
- zllen:压缩列表包含的节点数量。
- entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
- zlend:特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
entry用于存储实际数据,包含三部分内容:
- prevlen,记录了「前一个节点」的长度;
- encoding,记录了当前节点实际数据的类型以及长度;
- data,记录了当前节点的实际数据;
当我们往压缩列表中插入数据时,压缩列表就会根据数据是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。
2.7 QuickList(快速列表)
Quicklist 是 Redis 3.2 版本引入的一种高级数据结构,作为 List 类型的底层实现,它结合了双向链表和压缩列表(ziplist)的优点,在内存使用效率和操作性能之间取得了很好的平衡。
Quicklist 本质上是一个由多个 ziplist 组成的双向链表:
- 每个链表节点(quicklistNode)包含一个 ziplist
- 整个结构通过指针连接形成双向链表
这种混合设计带来了两大优势:
- 内存效率:ziplist 的紧凑存储减少了传统链表每个元素需要单独分配内存的开销
- 操作性能:保持了链表在插入/删除操作上的优势,同时通过 ziplist 提高了内存局部性
3、Redis对象
上面介绍了Redis的主要底层数据结构,包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表等。但是Redis并没有直接使用这些数据结构作为基本数据类型使用,而是基于这些数据结构创建了一些对象类型,也是我们熟悉的五大基本数据类型:字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set)。
每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个是字符串对象类型的键对象, 另一个是各种数据类型之一的值对象。每个值对象都由一个 redisObject 结构表示:
- type:对象类型,包含五大对象类型
- REDIS_STRING:字符串对象
- REDIS_LIST:列表对象
- REDIS_HASH:哈希对象
- REDIS_SET:集合对象
- REDIS_ZSET:有序集合对象
- encoding:编码方式,也就是底层数据结构。
- REDIS_ENCODING_INT:long 类型的整数
- REDIS_ENCODING_EMBSTR:embstr 编码的简单动态字符串
- REDIS_ENCODING_RAW:简单动态字符串
- REDIS_ENCODING_HT:字典
- REDIS_ENCODING_LINKEDLIST:双端链表
- REDIS_ENCODING_ZIPLIST:压缩列表
- REDIS_ENCODING_INTSET:整数集合
- REDIS_ENCODING_SKIPLIST:跳跃表和字典
- refcount:引用计数,用于内存回收和对象共享。当refcount为0时会进行对象回收,当新增一个键来共享已经存在的对象时,refcount加1。注意只有整数对象支持共享。
- ptr:指向底层实现数据结构的指针
注意type和encoding的区别:每种对象类型底层会有多种数据结构的实现。
类型 | 编码 | 对象 | 条件 |
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象。 | 保存的是整数值, 并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long ) |
ㅤ | REDIS_ENCODING_EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象。 | 保存的是字符串且长度<=44字节,只分配一次内存,在一块连续的内存空间中依次存储 redisObject 和 sdshdr 两个结构 |
ㅤ | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象。 | 保存的是字符串且长度>44字节,分配两次内存,分别存储redisObject 和 sdshdr 两个结构 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象。 | 当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
1. 列表对象保存的所有字符串元素的长度都小于 64 字节。
2. 列表对象保存的元素数量小于 512 个。
|
ㅤ | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象。 | 不满足以上任意条件
|
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象。 | 当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:
1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节。
2. 哈希对象保存的键值对数量小于 512 个。
|
ㅤ | REDIS_ENCODING_HT | 使用字典实现的哈希对象。 | 不满足以上任意条件
|
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象。 | 当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:
1. 集合对象保存的所有元素都是整数值。
2. 集合对象保存的元素数量不超过 512 个。
|
ㅤ | REDIS_ENCODING_HT | 使用字典实现的集合对象。 | 不满足以上任意条件
|
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象。 | 当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:
1. 有序集合保存的所有元素成员的长度都小于 64 字节。
2. 有序集合保存的元素数量小于 128 个。 |
ㅤ | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象。 | 不满足以上任意条件。
|
4、Redis的数据类型
类型 | 描述 | 常用命令 | 应用场景 |
String(字符串) | 最基本的数据类型,可以存储文本、数字或二进制数据(最大 512MB)。 | SET , GET , INCR , DECR , APPEND , STRLEN | 缓存、计数器、分布式锁 |
List(列表) | 有序的字符串集合,基于双向链表实现,支持两端插入和删除。 | LPUSH , RPUSH , LPOP , RPOP , LRANGE , LLEN | 消息队列、最新消息列表、历史记录、粉丝列表、文章的评论列表 |
Set(集合) | 无序不可重复的字符串集合,支持交集、并集、差集运算。 | SADD , SMEMBERS , SISMEMBER , SINTER , SUNION , SDIFF , SCARD | 标签系统、共同好友、去重数据 |
Hash(哈希表) | 键值对集合,键为不可重复的字符串,值为任意类型 | HSET , HGET , HMSET , HMGET , HGETALL , HDEL , HKEYS | 存储对象(如用户信息、商品详情) |
Sorted Set(有序集合) | 类似于 Set,但每个元素关联一个 score(分数),用于排序。 | ZADD , ZRANGE , ZREVRANGE , ZSCORE , ZRANK , ZREM , ZCARD | 排行榜、优先级队列、带权重的数据存储 |
Bitmap(位图) | 2.2 版新增,类似于一个以 Bit 为单位的数组,数组的每个单元只能存储 0 和 1,用少量空间记录大量状态(状态值只能为0或者),适合二值统计 | SETBIT , GETBIT , BITCOUNT , BITOP | 布隆过滤器、用户在线状态、签到统计 |
HyperLogLog(基数统计) | 2.8 版新增,用于高效估算集合的基数(不重复元素的数量)。使用固定的大小(不超过12k),用来统计海量数据中不重复的元素个数。基于伯努利算法,不会存储原始基数数据,用于基数统计,误差率约 0.81%。 | PFADD , PFCOUNT , PFMERGE | UV统计、大规模去重计数 |
GEO(地理空间) | 3.2 版新增,存储地理位置(经纬度),支持距离计算、范围查询。 | GEOADD , GEOPOS , GEODIST , GEORADIUS , GEOSEARCH | 附近的人、地理位置搜索 |
Stream(流) | 5.0 版新增,发布订阅(Pub/Sub)模式的升级版,类似于 Kafka 的消息队列,提供持久化、多消费者组等功能。 | XADD , XREAD , XGROUP , XRANGE , XACK | 事件日志、消息队列、实时数据处理 |
另外,Redis 的发布订阅(Pub/Sub)模式是一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。发布订阅模式并不是一种数据类型,存在的缺点:
- 没有ack机制
- 阻塞式的,一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。
- 没有持久化功能,消息发送后没有消费者消费就会丢失。
Redis 5.0 推出的 Stream 是基于发布订阅(Pub/Sub)模式的升级版,提供持久化、可靠消费、回溯消息等功能。Pub/Sub 适合轻量级场景,而 Stream 适合需要保证消息不丢失的复杂场景。
4.1 String
String 类型的底层的数据结构实现主要是整数集合和 SDS(简单动态字符串)。字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
- 如果一个字符串对象保存的是整数值,并且这个整数值可以用
long
类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr
属性里面(将void*
转换成 long),并将字符串对象的编码设置为int
。

- 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为
embstr
,embstr
编码是专门用于保存短字符串的一种优化编码方式。

- 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为
raw
。

可以看到
embstr
和raw
编码都会使用SDS
来保存值,但不同之处在于embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject
和SDS
,而raw
编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject
和SDS
。Redis这样做会有很多好处:embstr
编码将创建字符串对象所需的内存分配次数从raw
编码的两次降低为一次;
- 释放
embstr
编码的字符串对象同样只需要调用一次内存释放函数;
- 因为
embstr
编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
但是 embstr 也有缺点的:
- 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。
4.2 List
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
列表的最大长度为
2^32 - 1
,也即每个列表支持超过 40 亿
个元素。List 类型的底层数据结构是由双向链表或压缩列表实现的:
- 如果列表的元素个数小于
512
个(默认值,可由list-max-ziplist-entries
配置),列表每个元素的值都小于64
字节(默认值,可由list-max-ziplist-value
配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;

- 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。
4.3 Hash
Hash 是一个键值对(key - value)集合,其中 value 的形式如:
value=[{field1,value1},...{fieldN,valueN}]
。Hash 特别适合用于存储对象。Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
- 如果哈希类型元素个数小于
512
个(默认值,可由hash-max-ziplist-entries
配置),所有值小于64
字节(默认值,可由hash-max-ziplist-value
配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构; - 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。
- 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

- 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
4.4 Set
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储
2^32-1
个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。Set 类型的底层数据结构是由哈希表或整数集合实现的:
- 如果集合中的元素都是整数且元素个数小于
512
(默认值,set-maxintset-entries
配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;

- 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

4.5 Zset
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),相当于可排序的 Set。每个存储元素包含两个值:元素值和排序值。所以有序集合保留了集合不能有重复成员的特性(分值可以重复),且元素可以排序。
Zset 提供了以下特性:
- 按成员(Member)快速查找(类似 Set)。
- 按分值(Score)排序,并支持范围查找。
- 按排名(Rank)快速访问。
Zset 类型的底层数据结构组成:
- 如果有序集合的元素个数小于
128
个,并且每个元素的值小于64
字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构。在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

- 如果有序集合的元素不满足上面的条件,Redis 会使用两种数据结构相结合的方式来实现:
- 跳跃表(Skip List)
- 字典(Dict)
即一个 Zset 结构同时包含一个字典(Dict)和一个跳跃表(Skip List)。Zset 对象是唯一同时使用了两个数据结构来实现的 Redis 对象。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
在 Redis 源码(
server.h
)中,ZSet 的结构定义如下:dict
: 哈希表- 结构: Key 是 member,Value 是分值(score)的指针。
- 作用: 实现
O(1)
时间复杂度的高效成员(member)查找。给定一个 member,可以立即获取其对应的分值(score)。
zsl
: 跳跃表(Skip List)- 结构: 一个多层的有序链表。
- 作用: 维护一个根据 score 排序的成员列表。支持基于分值(score)和排名(rank)的范围操作,例如
ZRANGE
、ZRANK
、ZREVRANGEBYSCORE
等。
注意跳跃表的 obj 指针和字典的 key 指针是指向同一个对象,也就是数据只会保存一份。

为什么需要两种数据结构?
这是一种典型的 “以空间换时间” 的策略。
操作类型 | 如果只用 Hash | 如果只用 Skip List | Redis 的实际实现 (Hash + Skip List) |
根据 Member 查 Score (e.g., ZSCORE ) | O(1) | O(log N) | O(1) (通过 Hash) |
添加/删除/更新元素 (e.g., ZADD ) | O(1) (但无法排序) | O(log N) | O(log N) (主要耗时在更新 Skip List) |
按 Score 范围查询 (e.g., ZRANGEBYSCORE ) | 无法高效实现,需全表扫描 | O(log N) | O(log N) (通过 Skip List) |
按 Rank(排名)查询 (e.g., ZRANGE ) | 无法高效实现 | O(log N) | O(log N) (通过 Skip List) |
- 单一数据结构无法同时满足所有高频操作的高性能需求。
- 通过组合 Hash 和 Skip List,Redis 的 ZSet 在 查询分值、按分值排序、按排名访问 这三个核心操作上都达到了近乎最优的性能。
4.6 BitMap
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行
0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

4.7 HyperLogLog
Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
所以,简单来说 HyperLogLog 提供不精确的去重计数。
HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近
2^64
个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。这什么概念?举个例子给大家对比一下。
用 Java 语言来说,一般 long 类型占用 8 字节,而 1 字节有 8 位,即:1 byte = 8 bit,即 long 数据类型最大可以表示的数是:
2^63-1
。对应上面的2^64
个数,假设此时有2^63-1
这么多个数,从 0 ~ 2^63-1
,按照long
以及1k = 1024 字节
的规则来计算内存总数,就是:((2^63-1) * 8/1024)K
,这是很庞大的一个数,存储空间远远超过12K
,而 HyperLogLog
却可以用 12K
就能统计完。4.8 GEO
Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。
4.9 Stream
Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
- 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
- List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。
基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
- Author:mcbilla
- URL:http://mcbilla.com/article/0a9fbe68-677e-4538-a6e6-42f997de6811
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts