Redis 数据结构:从五种基础到四种高级

Redis 之所以强大,不只是因为快,更因为它提供了丰富的数据结构------就像仓库里不同形状的收纳盒,各有各的用处,选对了事半功倍。

五种核心数据类型

String

最基本的类型,一个 key 对应一个 value,二进制安全,能存文本、数字、序列化对象,理论上限 512MB。

  • 典型场景:缓存用户会话、页面数据;计数器(文章阅读量、点赞数,INCR 原子递增);分布式锁(SETNX + 过期时间)

  • 注意:存复杂对象需要序列化 / 反序列化,有额外开销;大 Value 是严重反模式(详见文末大 Key 部分)

Hash

键值对集合,一个 key 下面挂多个 field-value 对,天然适合存 对象属性

  • 典型场景:用户信息、商品详情------商品 ID 作为 key,价格、库存、名称等字段塞进同一个 Hash,改单个字段不用整体覆盖

  • 注意:小数据量下用压缩列表编码,非常省内存;field 数量超过阈值后会转成哈希表编码

List

有序字符串列表,底层是双向链表(3.2 后改为 quicklist),支持两端快速插入和删除。

  • 典型场景:简单消息队列(LPUSH 生产、RPOP 消费);最新消息列表;时间线 Feed

  • 注意:避免用 LRANGE 取大范围数据,元素过多时性能下降明显

Set

无序且元素不重复的集合,查找和去重效率高,支持交集、并集、差集运算。

  • 典型场景:标签系统;独立访客(UV)统计;共同关注、共同好友等社交关系计算

  • 注意:集合运算(SINTER、SUNION)在大集合上耗时较长,要注意使用场景

ZSet(Sorted Set)

和 Set 类似,但每个元素带一个 score 用于排序,有序 Set,底层用跳表 + 字典实现,插入和查询都很快。

  • 典型场景:排行榜------游戏积分榜、热搜榜,按 score 排序后直接取 Top N;带权重的任务队列

  • 注意:score 是 double 类型,有精度限制;相同 score 按 member 字典序排列

四种高级数据类型

随着 Redis 版本迭代,又陆续加入了四种高级类型,应对更特殊的场景。

BitMap(位图)

用 bit 位存储数据,一个 bit 只有 0 和 1 两种状态,极度节省空间。

  • 典型场景:签到打卡、用户在线状态、海量布尔统计------统计 1000 万用户的日活只需要约 1.2MB 空间

  • 本质:底层还是 String 类型,只是用位操作来读写

HyperLogLog

基数统计算法,用极小的内存(约 12KB)就能统计超大数量级的不重复元素数。

  • 典型场景:UV 统计、独立访客计数等不需要精确值的场景

  • 注意:有标准误差约 0.81%,追求精确计数不要用;本质也是 String 编码

GEO(地理空间)

底层基于 ZSet 实现,存储 经纬度信息,支持距离计算和范围查询。

  • 典型场景:"附近的人" "附近的商家" 等 LBS 功能,【LBS = Location Based Service,即"基于位置的服务"】

  • 注意:地球是球体,距离计算有近似误差,高精度场景需谨慎

Stream

Redis 5.0 引入的消息流类型,更专业的消息队列方案。

  • 典型场景:复杂消息队列、事件溯源、日志收集

  • 特点:支持消费者组、消息持久化、消息 ID 回溯,更接近 Kafka 的模型

底层编码:数据结构背后的数据结构

表面上是九种数据类型,底层实际由更少的编码结构组合而成。Redis 会根据数据量和元素大小自动选择编码,小数据用紧凑结构省内存大数据切换到高效结构保性能

三种关键底层结构

ziplist(压缩列表)

连续内存块组成的紧凑型结构,没有指针开销,非常省空间。小数据量的 List、Hash、ZSet 都用它。

但缺点是插入和删除需要移动内存,数据量大了性能会下降,所以达到阈值后会自动转换编码。

quicklist(快速列表)

Redis 3.2 之后 List 的默认实现,可以理解为"链表 + 压缩列表"的混合体------每个链表节点是一个 ziplist,既保留了 ziplist 的内存效率,又减少了普通双向链表的内存碎片和指针开销。

skiplist(跳跃表)

ZSet 的核心结构,在有序链表基础上增加了多层索引,把查找效率从 O(n) 提升到 O(log n)。配合一个字典用来做 O(1) 的成员查找,两者数据共享不重复存储。

编码自动转换

以 ZSet 为例,默认转换阈值:

  • 元素数量 ≤ 128 且每个元素大小 ≤ 64 字节 → 用 ziplist

  • 任一条件超出 → 转成 skiplist + dict

Hash 和 List 也有类似的自动转换机制,阈值都可以在配置文件里调整。

选型对比:这个需求该用哪种?

需求场景 推荐类型 为什么选它
存单个值、计数器、分布式锁 String 最简单直接,原子操作可靠
存对象的多个属性,经常修改单个字段 Hash 字段级操作,不需要整体序列化
消息队列、最新列表、按顺序存取 List / Stream List 简单够用,Stream 功能更全
去重、标签、共同好友、集合运算 Set 天然去重,集合运算高效
排行榜、带排序的集合 ZSet 按 score 排序,范围查询方便
签到、在线状态、海量布尔标记 BitMap 极度省空间,位运算高效
统计 UV、独立访客(不要求精确) HyperLogLog 12KB 搞定亿级基数统计
附近的人、地理位置查询 GEO 内置经纬度存储和距离计算

生产环境红线:警惕大 Key

虽然 String 理论上限 512MB,但生产环境存大 Value 是严重的反模式。大 Key 不只是 String 的问题,集合类型元素过多同样危险。

大 Key 判定标准

数据类型 大 Key 阈值 主要风险
String Value > 10KB 网络传输延迟,阻塞其他请求
Hash / Set / ZSet 元素数量 > 5,000 HGETALL / SMEMBERS 全量操作阻塞
List 元素数量 > 10,000 LRANGE 大范围操作耗时
Stream 消息数量 > 10,000 XRANGE 遍历性能下降

大 Key 的四大危害

  1. 命令阻塞:Redis 单线程模型,一个大 Key 操作慢了,后面所有请求都得等

  2. 网络瓶颈:大 Value 传输占带宽,一次 GET 可能就是几十上百兆流量

  3. 集群倾斜:大 Key 集中在某个分片,导致节点负载不均

  4. 持久化卡顿:BGSAVE / AOF 重写时 fork 大 Key 耗时长,可能引发阻塞

治理思路

  • 拆分:大对象拆成多个小 Key 分片存储;大集合按业务维度(如按日期、按用户)拆分

  • 压缩:Value 先压缩再存(如 gzip、snappy),用 CPU 换空间

  • 冷热分离:热数据留 Redis,冷数据下沉到 MySQL / ES 等持久化存储

  • 定期巡检 :用 redis-cli --bigkeysMEMORY USAGE 定期扫描发现大 Key


Redis 的数据类型看似只是"存东西的不同方式",实际上每种都对应着一类问题的最优解。理解了底层编码和选型逻辑,才能在项目里更好地用 Redis。

在时代的废墟上踉跄成长。-- 烟沙九洲