Redis 有哪些数据类型?各自的底层实现和使用场景是什么?
Redis 提供了丰富的数据类型,面试中经常考察每种类型的底层编码、转换条件以及典型场景。下面从五大数据类型讲起,再覆盖后续新增的特殊类型。
String:最基础的键值类型
String 是 Redis 最简单的数据类型,可以存储字符串、整数、浮点数,最大 512MB。
底层实现:Redis 没有直接使用 C 语言字符串,而是自己实现了 SDS(Simple Dynamic String)。SDS 在 C 字符串末尾 \0 的基础上增加了 len 和 alloc 字段:len 记录已用长度,alloc 记录分配总空间。这样做带来了三个好处——获取字符串长度从 O(N) 降为 O(1)、二进制安全(不依赖 \0 判断结尾)、空间预分配和惰性释放减少内存重分配次数。String 有三种编码:int(8字节以内整数)、embstr(44字节以内短字符串,一次分配内存)、raw(超过 44 字节,两次分配)。
使用场景:缓存用户信息或配置、分布式锁(SETNX + 过期时间)、计数器(INCR 原子自增)、Session 共享。
List:有序可重复的列表
List 是一个按插入顺序排序的字符串元素集合,支持从两端推入和弹出。
底层实现:Redis 3.2 之前用 ziplist(元素少时)或 linkedlist(元素多时)。3.2 之后统一改用 quicklist,它是一个由 ziplist 节点组成的双向链表,兼顾了 ziplist 的省内存和 linkedlist 的快速插入删除。Redis 7.0 进一步将 ziplist 替换为 listpack,解决了 ziplist 的级联更新问题。
使用场景:消息队列(LPUSH + BRPOP 实现阻塞队列)、最新消息列表(如朋友圈时间轴)、栈(LPUSH + LPOP)和队列(LPUSH + RPOP)操作。
Hash:字段-值映射
Hash 是键值对的集合,适合存储对象属性,类似 Java 的 HashMap。
底层实现:元素数量少且单个元素体积小时使用 listpack(原 ziplist),超过阈值切换为 hashtable。hashtable 采用链地址法解决哈希冲突,Redis 还实现了渐进式 rehash——维护 ht[0] 和 ht[1] 两个哈希表,rehash 期间每次增删改查操作都会顺带迁移一部分桶,避免一次性迁移造成阻塞。
使用场景:存储对象(用户信息、商品详情,比 String+JSON 更节省内存,可以只读写单个字段)、购物车(用户ID为key,商品ID为field,数量为value)。
Set:无序唯一集合
Set 是字符串元素的无序集合,元素不重复。
底层实现:当所有元素都是整数且数量不超过 512 时使用 intset(有序整数数组,内存紧凑),否则切换为 hashtable(value 存 null,只用 key)。
使用场景:标签系统、共同好友/共同关注(SINTER 取交集)、抽奖(SRANDMEMBER 随机取)、去重(SADD 自动去重)。
ZSet:有序唯一集合
ZSet 在 Set 的基础上每个元素关联一个 score,按 score 排序,是 Redis 中最复杂的数据类型之一。
底层实现:元素少且小时使用 listpack,否则使用 skiplist + hashtable 的组合。hashtable 提供 O(1) 的成员查找,skiplist 提供范围查询能力。跳表的层高通过随机算法确定(每层晋升概率 1/4),平均 O(logN) 查找。为什么不用红黑树?跳表实现更简单、范围查询更方便(只需要在底层链表上遍历)、插入删除只需修改相邻节点指针。
使用场景:排行榜(游戏积分、热搜榜)、延时队列(score 存到期时间,ZRANGEBYSCORE 取到期任务)、带权重的消息队列。
五大类型对比
| 类型 | 底层编码 | 是否有序 | 是否可重复 | 核心操作复杂度 |
|---|---|---|---|---|
| String | int/embstr/raw | - | - | O(1) |
| List | listpack/quicklist | 插入序 | 可重复 | 两端 O(1),中间 O(N) |
| Hash | listpack/hashtable | 无序 | field不可重复 | O(1) |
| Set | intset/hashtable | 无序 | 不可重复 | O(1) |
| ZSet | listpack/skiplist+hashtable | score有序 | 不可重复 | 查找O(1),范围O(logN+M) |
Bitmap:位级操作
Bitmap 不是独立的数据类型,而是 String 上的位操作扩展。
底层实现:基于 String(SDS),每个 bit 对应一个偏移量。SETBIT 将指定偏移位设为 0 或 1,BITCOUNT 统计设为 1 的位数。
使用场景:用户每日签到(一个用户一年只需 365 bit ≈ 46 字节)、统计连续签到天数、在线状态判断、布隆过滤器。
HyperLogLog:基数估算
HyperLogLog 用极小的内存估算集合基数(不重复元素数量),标准误差约 0.81%。
底层实现:基于概率算法,固定占用 12KB 内存(16384 个桶,每个 6 bit),不会随元素增多而增长。
使用场景:网站 UV 统计(百万级 UV 只需 12KB)、大屏数据去重计数。不需要精确值时优先选择,比 Set 存储节省几个数量级内存。
Geo:地理位置
Geo 用于存储地理位置信息并进行距离计算。
底层实现:基于 ZSet,使用 GeoHash 编码将经纬度转为 52 位整数作为 score。GEOADD 本质是 ZADD,GEORADIUS 本质是 ZRANGEBYSCORE + 距离计算。
使用场景:附近的人/店铺、距离计算、地理围栏。
Stream:消息流
Stream 是 Redis 5.0 新增的数据类型,专门为消息队列场景设计,可以看作轻量版 Kafka。
底层实现:使用 radix tree(基数树)+ listpack 实现。每条消息有全局递增的 ID(时间戳-序号),支持消费组(Consumer Group),同一条消息可被不同消费组各消费一次。
使用场景:消息队列(相比 List 的 BRPOP,Stream 支持消费组、消息确认、历史回溯,解决了 List 无法 ACK 和无法回溯的问题)、事件日志、实时数据管道。
与 List 做消息队列的区别:List 不支持消费组,一条消息只能被一个消费者取走;Stream 支持消费组,多条消息可分发到组内不同消费者并行处理,且消费者断线后未确认的消息可以转交给其他消费者。
面试高频追问:Redis 为什么用跳表不用红黑树实现 ZSet?——跳表实现简单、范围查询只需遍历底层链表、插入删除只需修改相邻指针,而红黑树的旋转操作更复杂且范围查询需要中序遍历。Hash 和 List 的编码转换阈值是多少?——Hash 在 field 数量超过 hash-max-ziplist-entries(默认128)或单个 field 超过 hash-max-ziplist-value(默认64字节)时从 listpack 转为 hashtable;List 的 quicklist 每个 node 的 ziplist 大小由 list-max-ziplist-size 控制。