Redis 中有哪些数据类型?(详解5种基础结构与4种高级结构及其底层原理)

Redis 之所以能成为当今最流行的键值存储系统,不仅因为其惊人的读写速度,更得益于其丰富且精心设计的数据结构体系。与传统的键值数据库仅支持字符串不同,Redis 提供了多种抽象数据类型,允许开发者直接在服务端进行复杂的数据操作,从而减少网络传输和客户端计算开销。很多初学者容易混淆“基础数据类型”与“高级数据结构”的概念,或者不清楚它们底层的编码实现,导致在生产环境中选型错误、内存浪费或性能瓶颈。本文将基于 Redis 7.0+ 的最新特性,全面解析 5 种基础数据结构和 4 种高级数据结构,深入剖析其底层原理、适用场景及最佳实践。

一、五大基础数据结构:核心基石与底层编码

Redis 的核心由五种基础数据类型构成,它们是构建所有复杂业务逻辑的原子单元。这五种类型分别是 String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 ZSet(有序集合)。值得注意的是,Redis 为了极致优化内存和性能,每种基础类型在底层都采用了多种编码方式,并根据数据量和内容动态切换。

1.1 String(字符串):万能的二进制安全容器

String 是 Redis 最基本的数据类型,也是唯一可以直接作为 Key 的类型。它的最大特点是二进制安全,意味着它可以存储任何内容,包括文本、JSON 对象、序列化后的二进制数据,甚至是一张图片的 Base64 编码。

底层原理与动态编码:
String 的底层实现是 SDS(Simple Dynamic String)。与 C 语言原生字符串不同,SDS 在结构体中显式记录了长度(len),使得获取长度的时间复杂度为 O(1),且彻底杜绝了缓冲区溢出问题。SDS 还采用了预分配冗余空间的策略,当字符串增长时,会多分配一些未使用的空间,减少频繁内存重分配的开销。
在 Redis 内部,String 根据存储内容的不同有三种编码:

  • int:当存储的是 64 位有符号整数时,直接以长整型存储,极度节省内存。
  • embstr:当存储的是小于 44 字节(Redis 7.0 之前是 39 字节)的短字符串时,将 SDS 头和字符串内容连续分配在一块内存中,减少一次指针寻址。
  • raw:当字符串超过 embstr 限制时,使用标准的 SDS 结构。

应用场景:
除了常规的缓存 KV 对,String 还支持原子增减操作(INCR/DECR),非常适合做分布式计数器(如文章阅读量、点赞数)。利用 SETNX 命令可以实现分布式锁。此外,通过位偏移操作,String 还可以退化为 Bitmap 使用(见高级结构部分)。

1.2 List(列表):双向链表与消息队列

List 是一个有序的字符串列表,按照插入顺序排序。你可以在列表头部(Left)或尾部(Right)添加或弹出元素。

底层原理与 QuickList 进化:
在 Redis 7.0 之前,List 的底层实现经历了从 ziplist(压缩列表)到 linkedlist(双向链表),再到 quicklist(快速列表)的演变。目前,List 统一由 QuickList 实现。QuickList 是双向链表和压缩列表(ZipList/ListPack)的结合体:它将链表分割成多个节点,每个节点内部是一个紧凑的压缩列表。这种设计既保留了链表易于扩展的特性,又利用了压缩列表内存连续、缓存友好的优势。
Redis 7.0 进一步引入了 ListPack 替代 ZipList,解决了 ZipList 在极端情况下的连锁更新(Cascade Update)问题,提升了内存安全性和修改效率。

应用场景:
List 是实现生产者 – 消费者模型消息队列的天然选择。利用 LPUSH + BRPOP 组合,可以轻松构建阻塞式消息队列。此外,它常用于存储用户的时间线数据(如微博动态、朋友圈消息),利用 LRANGE 分页读取最新内容。

1.3 Hash(哈希):对象存储的理想载体

Hash 是一个键值对集合,适合存储对象。例如,一个用户对象包含 id、name、age 等字段,可以将用户 ID 作为 Redis Key,字段名作为 Hash Field,字段值作为 Hash Value。

底层原理与编码切换:
Hash 的底层实现有两种编码:

  • ziplist/listpack:当字段数量较少(默认 512 个)且所有字段值和键的长度都较短(默认 64 字节)时,使用紧凑的压缩列表存储,极大节省内存。
  • hashtable:当数据量超过阈值,自动转换为标准的哈希表,保证 O(1) 的读写效率。

应用场景:
Hash 是存储用户信息、商品详情、配置项的首选。相比于将整个对象序列化为 JSON 存入 String,Hash 允许单独修改或获取某个字段(HSET/HGET),无需反序列化整个对象,节省了 CPU 和带宽。此外,Hash 支持 HINCRBY,可对对象中的数值字段进行原子累加。

1.4 Set(集合):去重与集合运算

Set 是一个无序的、元素唯一的字符串集合。它天然支持去重,并提供了强大的集合运算功能。

底层原理:
Set 的底层实现同样有两种:

  • intset:当集合中所有元素都是整数且数量较少时,使用整数数组存储,有序且紧凑。
  • hashtable:当包含非整数或数量过多时,转换为哈希表,Key 存元素,Value 统一为 NULL。

应用场景:
利用唯一性,Set 常用于抽奖系统(确保不重复中奖)、好友关系(共同好友计算)。其核心优势在于集合运算命令:SINTER(交集)、SUNION(并集)、SDIFF(差集)。例如,计算两个用户的共同兴趣标签(交集),或推荐可能认识的人(差集)。

1.5 ZSet(Sorted Set/有序集合):排行榜神器

ZSet 与 Set 类似,元素唯一,但每个元素关联了一个 double 类型的分数(Score),Redis 根据分数对元素进行自动排序。

底层原理:跳表与字典的双剑合璧:
ZSet 是唯一同时使用两种数据结构来维护数据的类型:

  • dict(字典):建立 Member 到 Score 的映射,保证通过成员查分数的速度为 O(1)。
  • skiplist(跳表):建立 Score 到 Member 的映射,保证数据有序,支持范围查询和排名操作,平均时间复杂度 O(log N)。
    这种双重结构使得 ZSet 既能快速定位,又能高效排序,是 Redis 中最复杂也最强大的数据结构。

应用场景:
ZSet 是实时排行榜(游戏积分榜、热搜榜)的标准解决方案。利用 ZREVRANGE 可获取前 N 名,ZREVRANK 可获取个人排名。此外,结合时间戳作为分数,还可实现延时队列或带权重的任务调度。

二、四大高级数据结构:特定场景的极致优化

除了上述五种基础类型,Redis 还提供了四种基于基础类型封装或特殊算法实现的“高级数据结构”。它们专为特定场景设计,能在内存占用或计算效率上达到极致。

2.1 Bitmap(位图):海量状态统计的省内存之王

Bitmap 并不是一种独立的数据类型,而是基于 String 类型的位操作扩展。它将字符串视为一个位数组,每个 bit 位(0 或 1)代表一种状态。

核心优势:
极致的内存压缩。传统方式存储一个布尔状态可能需要 1 字节甚至更多,而 Bitmap 仅需 1 bit。存储 1 亿用户的在线状态(0/1),仅需约 12MB 内存(100,000,000 / 8 / 1024 / 1024)。

操作与应用:
通过 SETBIT、GETBIT、BITCOUNT、BITOP 等命令,可以高效处理状态标记。

  • 用户签到:用一年的 365 个 bit 位记录用户签到情况,一个 Key 即可存储一个用户全年的签到数据。
  • 活跃用户统计:利用 BITOP 对多天的用户活跃位图进行“与/或”运算,快速计算出连续活跃用户或总活跃用户数。

2.2 HyperLogLog:基数统计的概率算法

HyperLogLog(HLL)用于进行基数统计(Cardinality Counting),即统计一组数据中不重复元素的个数(UV 统计)。

核心优势:
它以极小的内存占用(固定约 12KB)和允许极小误差(标准误差率 0.81%)为代价,换取了海量数据的统计能力。无论统计 100 个还是 100 亿个元素,占用的内存都是固定的 12KB。相比之下,用 Set 存储 10 亿个元素可能需要数 GB 内存。

操作与应用:
主要命令包括 PFADD(添加元素)、PFCOUNT(统计数量)、PFMERGE(合并统计)。

  • 网站 UV 统计:每天的用户访问 IP 或 UserID 直接打入 HLL,无需去重存储,轻松应对亿级流量。
  • 搜索关键词去重:统计某段时间内不同搜索词的数量。
  • 注意:HLL 只能统计数量,无法获取具体的元素列表,且存在微小误差,不适用于精确计数场景(如财务数据)。

2.3 Geo(地理空间):LBS 应用的内置引擎

Geo 是 Redis 3.2 引入的特性,本质上是 ZSet 的封装。它将经纬度坐标编码为 Geohash 字符串,并作为 Score 存入 ZSet 中。

核心优势:
无需引入额外的 GIS 数据库(如 MongoDB 或 PostGIS),即可在 Redis 中实现基础的地理位置服务。

操作与应用:

  • GEOADD:添加地理位置(经度、纬度、成员)。
  • GEODIST:计算两个成员之间的距离。
  • GEORADIUS / GEOSEARCH:查找指定半径范围内的成员(“附近的人”)。
  • GEOHASH:获取成员的 Geohash 值。
  • 应用场景:共享单车定位、外卖配送范围计算、社交软件“附近的人”功能。由于底层是 ZSet,它同样具备高性能排序和范围查询能力。

2.4 Stream(流):原生消息队列与事件溯源

Stream 是 Redis 5.0 引入的重磅特性,旨在提供类似 Kafka 的原生消息队列功能。它是一个真正的追加日志数据结构,支持多消费者组(Consumer Group)、消息确认(ACK)、阻塞读取和持久化。

核心优势:
弥补了 List 做消息队列时的不足(如缺乏 ACK 机制、消费进度难以管理)。Stream 保证了消息的至少一次投递(At Least Once),支持消息回溯(历史消息查询),并能自动维护消费者组的消费进度。

操作与应用:

  • XADD:追加消息。
  • XREAD / XREADGROUP:读取消息(支持阻塞)。
  • XACK:确认消息已处理。
  • XPENDING / XCLAIM:处理积压或失败的消息(故障转移)。
  • 应用场景:高可靠性的异步解耦、日志收集、事件溯源系统(Event Sourcing)、实时数据管道。对于需要严格保证消息不丢失且支持多消费者负载均衡的场景,Stream 是比 List 更优的选择。

三、选型指南与架构避坑策略

3.1 场景化选型决策矩阵

在实际开发中,如何选择合适的数据结构至关重要:

  • 简单缓存/计数器 -> String。
  • 对象存储/部分更新 -> Hash(优于 String 的 JSON 序列化)。
  • 消息队列 -> 简单场景用 List(LPUSH/BRPOP);高可靠、需 ACK 场景用 Stream。
  • 去重/集合运算 -> Set。
  • 排序/排行榜 -> ZSet。
  • 海量状态/签到 -> Bitmap。
  • UV 统计 -> HyperLogLog。
  • 位置服务 -> Geo。

3.2 内存优化与编码陷阱

虽然 Redis 会自动切换编码,但开发者仍需注意边界条件。例如,如果 Hash 中的字段名过长或数量刚好超过 hash-max-ziplist-entries 阈值,会导致瞬间从 listpack 升级为 hashtable,内存占用可能激增数倍。在设计 Key 结构时,应尽量控制字段数量和长度,使其保持在紧凑编码范围内。
对于 ZSet,若分数精度要求极高,需注意 Double 类型的精度限制。若成员名称过长,也会阻碍 ziplist 的使用。

3.3 大 Key(BigKey)风险预警

无论使用哪种数据结构,都要警惕 BigKey 的产生。一个包含百万级元素的 List 或 ZSet,在删除、序列化或网络传输时都可能阻塞主线程,导致服务抖动。

  • 检测:定期使用 --bigkeys 参数启动 redis-cli 扫描,或通过监控 COMMAND STATS 分析耗时命令。
  • 拆分:对于过大的集合,可采用分片策略(如 user:tags:1, user:tags:2…),在应用层进行聚合,避免单 Key 过大。

3.4 未来趋势:Redis 模块的扩展

除了内置数据结构,Redis 还支持 Module(模块)机制,允许开发者加载第三方插件来扩展新的数据类型,如 RedisJSON(原生支持 JSON 文档操作)、RedisSearch(二级索引与全文检索)、RedisBloom(布隆过滤器)。在复杂业务场景中,合理引入这些模块可以进一步简化架构,避免在应用层实现复杂的逻辑。

综上所述,Redis 丰富的数据结构体系是其强大生命力的源泉。深入理解每种结构的底层原理与适用边界,结合业务场景灵活选型,才能构建出既高效又稳定的缓存与存储系统。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部