5月28日 09:34

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 取到期任务)、带权重的消息队列。

五大类型对比

类型底层编码是否有序是否可重复核心操作复杂度
Stringint/embstr/raw--O(1)
Listlistpack/quicklist插入序可重复两端 O(1),中间 O(N)
Hashlistpack/hashtable无序field不可重复O(1)
Setintset/hashtable无序不可重复O(1)
ZSetlistpack/skiplist+hashtablescore有序不可重复查找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 控制。

标签:Redis