4、Redis基本数据结构:String、List、Set、ZSet、Hash 的底层原理与使用场景

说实话,Redis 之所以能成为缓存界的扛把子,很大程度上归功于它那五种基本数据结构。很多人觉得不就是个 key-value 嘛,能有多复杂?

但我在项目中踩过不少坑之后才明白——选对数据结构,性能能差出两个数量级。今天我就把这五种结构的底层原理和实战场景,掰开了揉碎了讲给你听。

4.1 String:最基础,也最容易用错

String 是 Redis 里最基础的数据结构。说白了,就是一个 key 对应一个 value,value 可以是字符串、数字,甚至是二进制数据(比如图片的 base64)。

底层原理:Redis 的 String 用的是 SDS(Simple Dynamic String),不是 C 语言的 char*。SDS 有个好处——它记录了字符串的长度,所以获取长度的时间复杂度是 O(1),而且它还能自动扩容,不会出现缓冲区溢出。

我个人的习惯:用 String 存 JSON 序列化后的对象,或者做计数器。但要注意,String 的 value 最大是 512MB,别往里塞大文件。

使用场景

  • 缓存用户信息(JSON 序列化后存储)
  • 计数器(比如文章阅读量、点赞数)
  • 分布式锁(SETNX 命令)
  • 限流(INCR + EXPIRE)
# 缓存用户信息
SET user:1001 '{"name":"张三","age":28}'
# 计数器
INCR article:readcount:2001
# 分布式锁
SET lock:order:1001 uuid NX EX 30

我曾经踩过的坑:用 String 存大文本,结果内存暴涨。后来才意识到,String 的底层是连续内存,大文本会导致内存碎片和频繁的 rehash。建议超过 10KB 的数据,考虑用 Hash 或者压缩后存储。

4.2 List:队列和栈的完美实现

List 在 Redis 里是一个双向链表。你可以从左边 push,也可以从右边 pop,操作非常灵活。

底层原理:Redis 3.2 之前,List 底层用的是 ziplist(压缩列表)或 linkedlist(双向链表)。3.2 之后引入了 quicklist,它把 ziplist 和 linkedlist 的优点结合了起来——每个节点是一个 ziplist,节点之间用双向指针连接。这样既节省了内存,又保证了操作效率。

你想想看,为什么不用数组?因为 List 经常在头部和尾部操作,数组在头部插入需要 O(n) 的时间,而链表只需要 O(1)。

使用场景

  • 消息队列(LPUSH + BRPOP)
  • 最新消息列表(LPUSH + LTRIM 限制长度)
  • 栈结构(LPUSH + LPOP)
  • 时间线(比如微博的时间线)
# 消息队列 - 生产者
LPUSH msg:queue '{"order_id":1001,"status":"paid"}'
# 消息队列 - 消费者(阻塞读取)
BRPOP msg:queue 0
# 最新消息列表,只保留最近100条
LPUSH news:list "news_content_101"
LTRIM news:list 0 99

避坑指南:List 做消息队列时,如果消费者挂了,消息会丢失。我建议配合 Redis 的 Stream 或者用专业的消息队列(如 RabbitMQ)来处理重要消息。List 适合做轻量级的、允许少量丢失的场景。

4.3 Set:去重和集合运算的利器

Set 是无序的字符串集合,它的最大特点就是元素不能重复。底层用的是哈希表(Hash Table),所以添加、删除、查找的时间复杂度都是 O(1)。

底层原理:当 Set 中的元素都是整数且数量不多时,Redis 会用 inset(整数集合)来存储,这是一种紧凑的数组结构,内存占用极低。当元素不是整数或者数量超过阈值时,自动切换为哈希表。

嗯,这里要注意:Set 是无序的,如果你需要有序的集合,请用 ZSet。

使用场景

  • 标签系统(给用户打标签,去重)
  • 好友关系(共同好友、关注列表)
  • 抽奖系统(SADD 添加参与者,SRANDMEMBER 随机抽取)
  • IP 白名单
# 给用户打标签
SADD user:1001:tags "篮球" "编程" "音乐"
# 获取共同好友
SINTER user:1001:friends user:1002:friends
# 随机抽奖
SADD lottery:2024 "user_001" "user_002" "user_003"
SRANDMEMBER lottery:2024 2  # 随机抽2个

我遇到过的一个场景:做电商的「你可能喜欢」功能,用 Set 存储用户浏览过的商品 ID,然后用 SDIFF 计算当前用户和相似用户之间的差集,推荐那些用户看过但当前用户没看过的商品。效果还不错。

4.4 ZSet:带权重的有序集合

ZSet(Sorted Set)是 Redis 里我最喜欢的数据结构。它给每个元素关联了一个 score(分数),然后按照 score 排序。底层用的是跳表(Skip List) + 哈希表。

底层原理:为什么用跳表而不是平衡树?说白了,跳表的实现更简单,而且范围查询的效率很高。跳表本质上是一种多层链表,通过「跳跃」的方式快速定位元素。平均时间复杂度是 O(log n),和平衡树差不多,但实现起来简单多了。

哈希表的作用是保证元素唯一性,同时支持 O(1) 的查找。

使用场景

  • 排行榜(游戏积分、文章热度)
  • 延迟队列(score 存时间戳)
  • 限流(滑动窗口)
  • 带权重的任务调度
# 游戏排行榜
ZADD game:score 10000 "player_001"
ZADD game:score 8500 "player_002"
ZADD game:score 12000 "player_003"
# 获取前三名
ZREVRANGE game:score 0 2 WITHSCORES
# 获取玩家排名
ZRANK game:score "player_001"
# 延迟队列 - 添加任务,score为执行时间戳
ZADD delay:queue 1700000000 "task_001"
ZADD delay:queue 1700000100 "task_002"
# 获取到期的任务
ZRANGEBYSCORE delay:queue 0 1700000000

我曾经犯过的错:用 ZSet 做排行榜时,score 用了浮点数。结果因为浮点数精度问题,导致排名出现偏差。后来我统一把 score 乘以 100 转成整数存储,问题就解决了。记住,能用整数就别用浮点数

4.5 Hash:对象存储的最佳选择

Hash 是一个键值对集合,类似于 Java 里的 HashMap。它特别适合存储对象——你可以把对象的每个字段存成一个 field-value 对。

底层原理:Hash 的底层有两种编码方式:ziplist(压缩列表)和 hashtable(哈希表)。当 Hash 中的字段数量少且每个字段的值比较短时,Redis 会用 ziplist,它把所有的 field-value 按顺序存储在一块连续内存里,非常节省空间。当字段数量或值大小超过阈值时,自动切换为 hashtable。

你想想看,为什么不用 String 直接存整个 JSON?因为 Hash 支持单独更新某个字段,而 String 需要整个反序列化、修改、再序列化。对于频繁更新部分字段的场景,Hash 的效率高得多。

使用场景

  • 用户信息(姓名、年龄、性别等字段)
  • 商品详情(价格、库存、描述等)
  • 购物车(商品 ID 为 field,数量为 value)
  • 配置信息
# 存储用户信息
HSET user:2001 name "李四" age 30 city "北京"
# 获取单个字段
HGET user:2001 name
# 获取所有字段
HGETALL user:2001
# 增加年龄
HINCRBY user:2001 age 1
# 购物车 - 添加商品
HSET cart:1001 "product_001" 2
HSET cart:1001 "product_002" 1

我的建议:对于对象存储,优先考虑 Hash。但要注意,如果 Hash 的字段非常多(比如上千个),ziplist 的查询效率会下降,因为它是顺序查找。这时候可以考虑拆分成多个 Hash,或者用 String 存储序列化后的数据。

4.6 五种数据结构对比总结

数据结构 底层实现 有序性 唯一性 时间复杂度 典型场景
String SDS O(1) 缓存、计数器、分布式锁
List quicklist 有(插入顺序) O(1) 头尾操作 消息队列、最新列表
Set inset / hashtable O(1) 标签、去重、集合运算
ZSet 跳表 + 哈希表 有(按 score) O(log n) 排行榜、延迟队列
Hash ziplist / hashtable field 唯一 O(1) 平均 对象存储、购物车

最后说一句,这五种数据结构不是孤立的。我在实际项目中经常组合使用——比如用 Hash 存用户信息,用 ZSet 维护用户积分排行榜,用 Set 做标签系统。选对数据结构,你的 Redis 性能至少能提升 50%。

下一章我会讲 Redis 的过期策略和内存淘汰机制,这可是面试高频题,也是线上事故的高发区。到时候我会分享一个我亲身经历的「缓存雪崩」事故,保证让你印象深刻。