Redis对象类型与底层数据结构

一、Redis对象类型概述

1.1 Redis数据类型总览

Redis提供了丰富的数据类型,用于不同的业务场景:

对象类型 说明 典型场景
String 字符串 缓存、计数器、分布式锁
List 双向链表 队列、消息队列、最新列表
Hash 哈希表 存储对象、购物车
Set 无序集合 好友关系、抽奖
Zset 有序集合 排行榜
Stream 流数据类型 消息队列、事件流
HyperLogLog 基数统计 UV统计

二、String类型详解

2.1 三种底层编码

查看命令:OBJECT ENCODING key

复制代码
String类型根据数据特性分为三种底层实现:

+-----------------------------------------------+
|              String 类型                       |
+-----------------------------------------------+
|                                                 |
|  int               raw               embstr     |
|  |                 |                 |          |
|  |                 |                 |          |
|  v                 v                 v          |
| 整数          长字符串       短字符串             |
| 20字节以内      大于44字节     44字节以内          |
+-----------------------------------------------+

2.2 int编码

触发条件:

  • 字符串长度小于等于20字节
  • 内容是纯整数

示例:

复制代码
SET num 12345678901234567890
OBJECT ENCODING num
-> "int"

存储形式:直接以long型整数存储,节省内存

特点:

  • 直接存储为C语言的long类型
  • 最高效的存储方式
  • INCR/INCRBY等命令直接在整数上操作

2.3 raw编码

触发条件:

  • 字符串长度大于44字节

示例:

复制代码
SET article "这是一段很长的文章内容..."
        (长度超过44字节)
OBJECT ENCODING article
-> "raw"

存储形式:SDS(Simple Dynamic String)动态字符串

SDS结构:

复制代码
struct sdshdr {
    int len;      // 已用长度
    int free;     // 剩余空间
    char buf[];   // 实际存储字符的数组
}

为什么是44字节?

复制代码
embstr和raw的分界线是44字节,原因:
  - embstr只分配一次内存(header + data连续)
  - raw需要分配两次内存(header和data分开)
  - 44字节时embstr刚好能用一次内存分配
  - 超过44字节,embstr反而浪费内存

2.4 embstr编码

触发条件:

  • 字符串长度小于等于44字节

示例:

复制代码
SET name "zhangsan"
OBJECT ENCODING name
-> "embstr"

存储形式:嵌入式字符串,一次内存分配

内存布局对比:

复制代码
raw编码(两次内存分配):
+--------+-------------+
| sdshdr |   SDS头     |
+--------+-------------+
| char[] |  实际字符串  |  <- 另一块内存
+--------+-------------+

embstr编码(一次内存分配):
+---------------------+
|  redisObject  |  sdshdr    |
+---------------+-------------+
|               |  char[]    |
|     整体连续   |  实际字符串 |
+---------------+-------------+

embstr的优势:

  • 只需一次内存分配,效率更高
  • 数据连续存储,缓存友好
  • 适合短字符串存储

三、List类型详解

3.1 quicklist双向链表

复制代码
List的底层实现是quicklist:
  本质上是双向链表
  每个节点是ziplist(压缩列表)
  兼顾快速访问和紧凑存储

quicklist结构:

复制代码
+---------+---------+---------+---------+---------+
| head   |   ...   |  node   |   ...   |  tail   |
| pointer|         |  +----+ |         | pointer |
+---------+---------+--+----+-+---------+---------+
                      |
                      v
              +---------------+
              |     ziplist   |
              |  (压缩列表)    |
              +---------------+

3.2 ziplist压缩列表

ziplist的特点:

  • 连续内存块,节省空间
  • 每个节点记录前一个节点长度,支持双向遍历
  • 内存布局紧凑

ziplist结构:

复制代码
+--------+---------+---+-------+---+-------+---+----+
|zlbytes |zltail   |zllen| entry1 |...| entryN |zlend|
|总长度  |末元素偏移|数量|  内容  |...|  内容  |0xFF|
+--------+---------+---+-------+---+-------+---+----+

节点entry结构:

复制代码
+---------+--------------+--------+
| previous|    content   |  encoding  |
| length  |   (数据)      | (编码)     |
+---------+--------------+--------+
   |
   └── 记录前一个节点长度,支持从后向前遍历

压缩列表的局限:

  • 新增/修改需要移动后续元素
  • 节点过长时效率下降

四、Hash类型详解

4.1 两种底层编码

复制代码
Hash类型根据数据量自动选择编码:

+---------------------------------------------+
|              Hash 类型                       |
+---------------------------------------------+
|                                               |
|         元素数量 <= 512                       |
|         且字段长度 <= 64字节                   |
|              |                               |
|              v                               |
|         ziplist(压缩列表)                   |
|         (优化存储空间)                        |
|              |                               |
|              | 超过任一条件                   |
|              v                               |
|         dict(哈希表)                        |
|         (优化查询性能)                        |
+---------------------------------------------+

4.2 ziplist编码

触发条件:

  • 节点数量小于等于512
  • 每个字段字符串长度小于等于64字节

存储结构:

复制代码
key: "***"
+--------+---------+---+-------+---+-------+---+----+
| field1 | value1  |...|field2 |...|field3 |...|zlend|
| "name" | "zhang" |   | "age" |   |"city"  |   |0xFF |
+--------+---------+---+-------+---+-------+---+----+

ziplist存储Hash的特点:

  • 字段和值交替存储
  • 新增字段到列表头部
  • 节省内存,但查询需要遍历

4.3 dict编码

触发条件:

  • 节点数量大于512
  • 或字段字符串长度大于64字节

dict结构(哈希表):

复制代码
+-----------------------------------------+
|              dict结构                   |
+-----------------------------------------+
|  hashTable[]                            |
|  +--------+--------+--------+--------+  |
|  | slot 0 | slot 1 | slot 2 | slot 3 |  |
|  +--------+--------+--------+--------+  |
|        |                                |
|        v                                |
|  +----------------+                     |
|  | dictEntry      |                     |
|  | key -> value   |                     |
|  +----------------+                     |
+-----------------------------------------+

dict的特点:

  • O(1)时间复杂度查询
  • 内存开销比ziplist大
  • 适合大量字段的场景

五、Set类型详解

5.1 两种底层编码

复制代码
Set类型根据元素特性选择编码:

+---------------------------------------------+
|              Set 类型                        |
+---------------------------------------------+
|                                               |
|         元素都是整数                          |
|         且元素数量 <= 512                      |
|              |                               |
|              v                               |
|         intset(整数集合)                   |
|              |                               |
|              | 不满足                         |
|              v                               |
|         dict(字典)                          |
+---------------------------------------------+

5.2 intset整数集合

触发条件:

  • 所有元素都是整数
  • 元素数量不超过512个

intset结构:

复制代码
+--------+--------+--------+----------------+
|encoding|length  |  []    |  整数数组       |
| INT16  |  5     |0 3 5 7 |  按升序排列      |
+--------+--------+--------+----------------+

特点:

  • 使用紧凑的整数数组存储
  • 节省内存
  • 支持二分查找

5.3 dict编码

触发条件:

  • 元素包含字符串
  • 或元素数量超过512

dict实现Set:

复制代码
+-----------------------------------------+
|  dict实现Set                             |
+-----------------------------------------+
|  key = ***                          |
|  value = NULL(只用key)                 |
|                                           |
|  特性:                                   |
|  1. 元素唯一性:key不重复                |
|  2. 元素无序:通过hash保证唯一            |
|  3. 快速查找:O(1)时间复杂度              |
+-----------------------------------------+

Set特有操作(差集、交集、并集)的实现:

复制代码
SINTER setA setB
    |
    +-- 遍历setA的dict
    +-- 检查元素是否在setB的dict中
    +-- 返回都存在的元素

SDIFF setA setB
    |
    +-- 遍历setA的dict
    +-- 检查元素是否不在setB中
    +-- 返回setA有setB没有的元素

六、Zset类型详解

6.1 两种底层编码

复制代码
Zset根据数据量选择编码:

+---------------------------------------------+
|              Zset 类型                       |
+---------------------------------------------+
|                                               |
|         元素数量 <= 128                       |
|         且member长度 <= 64字节                |
|              |                               |
|              v                               |
|         ziplist(压缩列表)                   |
|         (每个元素同时存储member和score)      |
|              |                               |
|              | 超过任一条件                   |
|              v                               |
|         skiplist + dict                      |
|         (跳表排序 + 字典快速查找)             |
+---------------------------------------------+

6.2 ziplist编码

触发条件:

  • 元素数量小于等于128
  • 每个member长度小于等于64字节

存储结构:

复制代码
+---------+---------+---+-------------+---+-------------+---+----+
| member1 | score1  |...|  member2    |...|  member3    |...|zlend|
| "alice" |  850    |   | "bob"       |   | "carol"     |   |0xFF |
+---------+---------+---+-------------+---+-------------+---+----+
           score按从小到大排列

特点:

  • member和score交替存储
  • 按score排序
  • 节省内存

6.3 skiplist跳表编码

触发条件:

  • 元素数量大于128
  • 或member长度大于64字节

跳表结构:

复制代码
Level 3:  [head] ----------------------> [node: carol]
Level 2:  [head] ----> [node: bob] ----> [node: carol]
Level 1:  [head] ----> [node: alice] --> [node: carol]
Level 0:  [head] ----> [node: alice] --> [node: bob] -----> [node: carol]

跳跃:alice(850) -> bob(950) -> carol(1200)

跳表实现原理:

复制代码
跳表是一种多级索引的结构:
  Level 0: 包含所有元素(有序)
  Level 1: 每隔一个元素选一个
  Level 2: 每隔两个元素选一个
  ...

查找:从最高层开始,逐层向下逼近
  1. 从Level 3开始,发现alice < target,继续前进
  2. 到达bob < target,继续前进
  3. carol > target,下降到Level 2
  4. ... 逐步定位到目标位置

时间复杂度:
  - 查找:O(log n)
  - 插入:O(log n)
  - 删除:O(log n)

空间复杂度:
  - 每层节点数递减:n/2 + n/4 + n/8 + ... = 2n
  - 空间复杂度O(n)

为什么Zset需要skiplist + dict?

复制代码
Zset需要支持两种操作:
  1. 按排名查找:ZRANGE 0 9  -> 需要跳表
  2. 按member查score:ZSCORE -> 需要dict

单一数据结构的不足:
  - 纯跳表:无法O(1)查member
  - 纯dict:无法按排名有序

解决方案:skiplist + dict组合
  +-----------------------------------------+
  |  Zset同时维护两个结构:                  |
  |                                           |
  |  跳表:member -> score (用于排序)        |
  |  字典:member -> score (用于快速查找)    |
  |                                           |
  |  更新时同时维护两个结构                   |
  |  空间换时间思想                          |
  +-----------------------------------------+

内存布局:

复制代码
+-----------------------------------------------+
|                    skiplist节点                |
+-----------------------------------------------+
|  dictEntry (key=*** value=score)             |
|  +--------------------------------------------+
|  |  struct zset {                             | |
|  |      dict *dict;      // member -> score  | |
|  |      skiplist *zsl;   // 排序用            | |
|  |  };                                        | |
|  +--------------------------------------------+
+-----------------------------------------------+

dict和skiplist共享member字符串,score在两处都存储

七、编码转换规则汇总

7.1 String类型转换

条件 编码 说明
长度<=20且为整数 int 整数存储,最省空间
长度<=44 embstr 一次内存分配,高效
长度>44 raw SDS动态字符串

7.2 List类型转换

条件 编码
默认 quicklist(ziplist节点)

7.3 Hash类型转换

条件 编码 说明
元素<=512 且 字段<=64字节 ziplist 节省空间
元素>512 或 字段>64字节 dict 优化查询性能

7.4 Set类型转换

条件 编码 说明
全整数且元素<=512 intset 紧凑存储
其他情况 dict 支持任意类型

7.5 Zset类型转换

条件 编码 说明
元素<=128 且 member<=64字节 ziplist 节省空间
元素>128 或 member>64字节 skiplist+dict 兼顾排序和查找

八、Redis数据结构设计思想

8.1 为什么需要动态选择编码?

内存与性能的权衡:

复制代码
Redis是内存数据库:
  - 内存是稀缺资源
  - 需要尽可能节省内存占用

单线程+事件循环:
  - 需要快速响应命令
  - 操作延迟直接影响性能

两种优化策略:

复制代码
策略1:空间优化(节点少时)
  +-- ziplist:紧凑连续存储,节省内存
      但插入删除需要移动元素,O(n)

策略2:性能优化(节点多时)
  +-- dict/skiplist:指针跳转,O(1)或O(log n)
      但需要额外内存存储索引

Redis的选择:
  数据少时,用空间换时间(ziplist)
  数据多时,用时间换空间(dict/skiplist)

8.2 空间换时间思想的应用

场景1:Zset的skiplist + dict

复制代码
空间开销:
  - 跳表节点:每层指针 + score + member
  - 字典节点:key + value + 指针

时间收益:
  - 按排名查找:O(log n) 跳表
  - 按member查找:O(1) 字典

结论:用2倍内存换取高效查询

场景2:quicklist的ziplist节点

复制代码
默认每个ziplist节点最大64KB
  - 节点太小:指针开销大
  - 节点太大:内存碎片多

配置参数:list-max-ziplist-size -2
  - -2表示单个节点最大8KB

8.3 编码转换的影响

转换是单向的:

复制代码
Hash: ziplist -> dict
  一旦数据量超过阈值,变成dict后
  即使数据减少,也不会变回ziplist

Set: intset -> dict
  同理,dict不会变回intset

业务建议:

  • 数据量稳定的场景:编码转换影响小
  • 数据量波动的场景:提前预估容量

九、查看Redis内部结构

9.1 查看对象编码

bash 复制代码
# 查看单个key的编码类型
OBJECT ENCODING key

# 查看key的内存占用
MEMORY USAGE key

# 查看key的所有信息
DEBUG OBJECT key

9.2 编码类型对照表

OBJECT ENCODING返回值 实际数据结构
int 整数
embstr 嵌入式字符串
raw SDS动态字符串
quicklist 快速列表
ziplist 压缩列表
intset 整数集合
hashtable 哈希表
skiplist 跳表

9.3 配置参数

bash 复制代码
# String类型阈值(一般不需调整)
string-max-int-size 512

# Hash类型阈值
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# List类型阈值(每个ziplist节点大小)
list-max-ziplist-size -2

# Set类型阈值
set-max-intset-entries 512

# Zset类型阈值
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

十、面试追问FAQ

问题 答案
Redis为什么用跳表而不是红黑树? 跳表实现简单,区间查找更高效,调节参数可控制性能
为什么要两种编码? 数据少用空间优化,数据多用性能优化,动态切换
embstr为什么限制44字节? embstr一次内存分配,44字节刚好能用一次分配完成
压缩列表的优缺点? 优点:内存紧凑;缺点:插入删除需要移动元素
ziplist什么时候会连锁更新? 连续插入/删除长度接近255的节点时可能触发
Redis对象和底层编码的关系? 一个对象类型可以对应多种编码,根据数据量动态选择

根据零声教育教学写作https://github.com/0voice

相关推荐
倔强的石头_3 小时前
深度解析:数据库内核如何通过逻辑推理与常值推导突破去重性能瓶颈
数据库
devilnumber3 小时前
MySQL 部门表:树结构 (自关联) vs 非树结构 (扁平化 / 冗余字段)
数据库·mysql
Hesionberger3 小时前
LeetCode114:二叉树展开为链表(三解法)
数据结构
一行代码一行诗++3 小时前
循环的嵌套
数据结构·算法
fengxin_rou3 小时前
【MySQL 三大日志深度解析】:redo log、undo log、binlog 作用与两阶段提交原理
数据库·mysql·日志·redo log
ECT-OS-JiuHuaShan3 小时前
存在是微分张量积,标量是参数但不可能是本质。还原论泛化,是语义劫持和以偏概全的逻辑谋杀伪科学庞氏骗局
数据库·人工智能·算法·机器学习·数学建模
IT策士3 小时前
Django 从 0 到 1 打造完整电商平台:使用 Django 消息框架与用户权限初步
数据库·django·sqlite
爱莉希雅&&&3 小时前
Redis哨兵模式和主从复制和集群模式搭建与扩容缩容
linux·redis·缓存·集群·哨兵·数据库同步
星河耀银海3 小时前
JAVA 注解(Annotation):从原理到实战应用
java·开发语言·数据库