Redis 数据类型总结
Redis 是一个高性能的内存数据库,它之所以强大,不只是因为数据存放在内存里,还因为它针对不同业务场景设计了丰富的数据类型和高效的底层数据结构。
很多人背 Redis,只会说五种基础类型:String、Hash、List、Set、ZSet。
但如果面试官继续问:
- 它们分别适合什么场景?
- 底层是怎么实现的?
- 为什么 Redis 这么快?
- Hash 为什么小时候用 listpack,大了变成哈希表?
如果答不上来,说明理解还不够深入。
这篇文章就系统梳理一下 Redis 的数据类型和底层实现。
一、Redis 常见数据类型总览
Redis 常见的数据类型主要有:
- String
- Hash
- List
- Set
- ZSet
除此之外,还有一些特殊类型或高级结构:
- Bitmap
- HyperLogLog
- Geospatial
- Stream
面试里最核心的还是前五种,所以重点先放在这五类。
二、String
1. String 是什么
String 是 Redis 中最基础、最常用的数据类型。
它的值本质上就是一段字符串,但这个字符串并不一定只能存文本,也可以存数字、二进制数据、序列化后的对象等。
2. 典型使用场景
String 常见用途包括:
- 普通缓存
- 计数器
- 分布式锁
- token / session 存储
- 验证码
- 限流计数
比如:
- 缓存用户信息
- 用
INCR统计访问次数 - 用
SET key value NX EX做分布式锁
3. 底层实现
String 的核心底层是 SDS ,也就是 Simple Dynamic String。
Redis 不直接使用 C 语言原生字符串,而是自己实现了一套动态字符串结构。
SDS 相比普通 C 字符串有几个明显优点:
第一,获取长度是 O(1)
C 字符串依赖 \0 判断结尾,所以获取长度往往需要遍历。
SDS 会直接记录当前长度,因此取长度复杂度是 O(1)。
第二,二进制安全
C 字符串很依赖 \0 作为结束符,不适合安全存储二进制数据。
SDS 不依赖这个机制,因此可以安全存储图片、序列化对象等二进制内容。
第三,减少频繁扩容
SDS 在扩容时通常不会只扩"刚刚够用"的那一点,而是会额外预留一些 free 空间。
这样后续继续 append 时,很多情况下就不需要再次申请内存和拷贝数据。
这也是为什么面试里常问:
SDS 为什么比普通字符串更适合 Redis?
核心答案就是:记录长度、二进制安全、空间预分配。
三、Hash
1. Hash 是什么
Hash 可以理解成一个 field-value 的映射表 。
它很适合存一个对象的多个属性。
比如一个用户对象:
- name -> Alice
- age -> 20
- city -> Beijing
在 Redis 里可以存成一个 Hash。
2. 典型使用场景
Hash 特别适合:
- 存储用户信息
- 存储商品信息
- 存储配置项
- 存储对象属性
和 String 直接存 JSON 相比,Hash 的一个优势是:
你可以只修改某个字段,而不用整体重写整个对象。
3. 底层实现
Hash 在 Redis 中不是始终只有一种底层实现。
小数据量时:listpack
当字段数量较少、field 和 value 都比较短时,Hash 会采用更紧凑的 listpack 结构。
这样做的好处是节省内存。
数据变大后:dict
当字段数量变多,或者 field/value 变长后,Hash 会切换成底层的 哈希表 ,也就是 Redis 源码里的 dict 实现。
所以面试里如果有人问:
Hash 为什么小的时候用 listpack,大了变成 dict?
你可以答:
因为 Redis 会在"节省内存"和"提高查找效率"之间做平衡。小对象用 listpack 更省空间;大对象再顺序扫描就太慢了,所以会转成哈希表,提高 field 查找效率。
4. 这里的 dict 是什么
Redis 里的 dict,本质上就是它自己实现的一套哈希表。
大致结构可以理解成:
- 一个 table 数组
- 数组每个桶里挂着链表节点
- 发生冲突时用链地址法处理
而且 Redis 的 dict 还支持 渐进式 rehash 。
也就是说,当扩容或缩容时,它不会一次性把所有数据搬迁完,而是分多次逐步迁移,避免长时间阻塞。
四、List
1. List 是什么
List 是一个有序列表,可以在两端插入和弹出元素。
它有点像双端队列。
2. 典型使用场景
List 常用于:
- 消息队列
- 时间线
- 最新消息列表
- 简单任务队列
比如:
LPUSH生产消息RPOP消费消息
虽然现在更正式的消息系统一般会用 Stream、MQ 或 Kafka,但 List 依然是很经典的队列型结构。
3. 底层实现
Redis 现在的 List 主要底层实现是 quicklist。
quicklist 可以理解成:
一个双向链表,链表的每个节点里再存一个紧凑结构。
这样设计的原因是:
- 普通链表插入删除灵活,但内存开销大
- 单个连续数组虽然紧凑,但插入删除不灵活
- quicklist 试图兼顾两者优点
所以它既保留了链表的灵活性,又尽量减少了纯链表节点的内存浪费。
五、Set
1. Set 是什么
Set 是一个无序且元素唯一的集合。
它不允许重复元素,因此天然适合做去重类场景。
2. 典型使用场景
Set 常用于:
- 去重
- 共同好友
- 标签集合
- 判断元素是否存在
- 抽奖名单
比如:
- 统计某个活动参与用户去重
- 求两个用户共同关注的人
- 判断一个商品是否被某用户收藏
3. 底层实现
Set 的底层也不是固定一种。
小且全是整数时:intset
如果集合元素都是整数,且数量比较少,Redis 会用 intset 。
它是一种更省内存的整数集合结构。
一般场景:dict
如果元素不是纯整数,或者元素数量增多,Set 会转成 dict 来实现。
不过这里的 dict 和 Hash 类型里的 dict 用法稍有不同。
对于 Set 来说,通常是:
- 元素本身作为 key
- value 用固定占位值
这样就能利用哈希表快速判断某个元素是否存在。
六、ZSet
1. ZSet 是什么
ZSet 是 有序集合 。
它和 Set 一样,元素不能重复;但它比 Set 多了一个 score 分数,用来排序。
2. 典型使用场景
ZSet 特别适合:
- 排行榜
- 热度榜
- 延迟队列
- 权重排序
- 范围查找
比如:
- 游戏积分排行
- 视频热度榜
- 按时间戳做定时任务扫描
3. 底层实现
小数据量时:listpack
数据少、元素短的时候,ZSet 会用 listpack。
数据变大后:skiplist + dict
当数据量变大时,ZSet 一般会同时使用:
- 跳表(skiplist)
- 哈希表(dict)
为什么要同时用两种结构?
因为 ZSet 既要支持:
- 根据 member 快速找到它的 score
- 又要支持按照 score 排序、范围查找
单靠一种结构很难两边都兼顾。
所以它会这样做:
- dict:负责 member -> score 的快速定位
- skiplist:负责按 score 有序存储,方便排序和区间查询
这就是 ZSet 很经典的面试点。
七、Redis 的特殊类型
除了五种基础类型,Redis 还有一些常见扩展能力。
1. Bitmap
本质上是基于 String 的位操作。
适合做:
- 用户签到
- 活跃标记
- 布尔状态压缩存储
2. HyperLogLog
适合做基数统计,比如:
- UV 统计
- 独立访客数量估算
它的优点是节省内存,但统计结果是近似值,不是绝对精确。
3. Geospatial
适合存储地理位置并做附近搜索。
底层本质上还是基于有序集合扩展实现。
4. Stream
Redis 5 引入,适合做更正式一点的消息流场景。
相比 List,它在消息队列语义上更完整,支持消费组等机制。
八、Redis 常见底层数据结构总结
Redis 常见底层结构主要有:
- SDS
- dict
- quicklist
- skiplist
- intset
- listpack
可以简单对应成这样:
| Redis 类型 | 常见底层实现 |
|---|---|
| String | SDS |
| Hash | listpack / dict |
| List | quicklist |
| Set | intset / dict |
| ZSet | listpack / skiplist + dict |
九、Redis 为什么快
这个问题几乎一定会被追问。
Redis 快,不只是因为它是内存数据库,还因为:
1. 数据在内存里
这是最直接的原因。
内存访问速度远快于磁盘。
2. 底层数据结构高效
Redis 针对不同场景设计了合适的数据结构,比如:
- String 用 SDS
- ZSet 用 skiplist + dict
- 小对象用 listpack 节省空间
3. 单线程模型减少锁竞争
Redis 的命令执行主线程是单线程模型,很多操作天然避免了复杂锁竞争。
4. IO 多路复用
Redis 网络层使用 IO 多路复用,提高了大量连接场景下的处理效率。
所以更完整的表述应该是:
Redis 快,不只是因为在内存里,也因为底层数据结构设计高效,并且网络和执行模型都比较轻量。
十、面试高频易错点
1. Redis 数据类型 ≠ Redis 底层数据结构
这是最容易混的。
比如:
- String、Hash、List、Set、ZSet 是数据类型
- SDS、dict、quicklist、skiplist 是底层数据结构
面试里一定要区分清楚。
2. Redis 不是"所有数据都靠 dict"
dict 很重要,但 Redis 整体不是只由 dict 组成。
更准确地说:
- 数据库层面 key 到 value 的映射通常靠 dict
- 但 value 具体底层还会根据类型走不同结构
比如:
- String -> SDS
- Hash -> listpack / dict
- List -> quicklist
- Set -> intset / dict
- ZSet -> skiplist + dict
3. Hash 和 Set 底层都可能用 dict,但含义不一样
Hash 用 dict 时,通常是:
- field -> value
Set 用 dict 时,通常是:
- element -> 占位值
所以虽然底层都能用 dict,但逻辑含义不同。
十一、面试版标准回答
如果面试官问:
"Redis 的数据类型和底层实现你讲一下。"
你可以这样答:
Redis 常见的数据类型主要有 String、Hash、List、Set 和 ZSet。
String 常用于缓存、计数器和分布式锁;Hash 适合存对象属性;List 适合消息队列;Set 适合去重;ZSet 因为带 score,常用于排行榜。
底层实现上,Redis 不同类型会根据数据规模选择不同结构。比如 String 核心是 SDS;Hash 小数据量时会用 listpack,数据变大后会转成哈希表 dict;List 主要用 quicklist;Set 小整数集合时用 intset,否则一般用 dict;ZSet 小数据量时用 listpack,数据量大后一般用 skiplist 加 dict。
所以 Redis 性能好,不只是因为它在内存里,也因为底层数据结构设计比较高效。
十二、总结
Redis 的强大之处,不只是"快",而是它根据不同业务需求提供了多种高效的数据抽象。
- String 适合最通用的缓存和计数
- Hash 适合对象属性存储
- List 适合顺序型数据和简单队列
- Set 适合去重和集合运算
- ZSet 适合排序和排行榜
而在底层,Redis 又会根据数据规模和场景,在:
- listpack
- dict
- quicklist
- skiplist
- intset
- SDS
这些结构之间做平衡。