← 返回题目列表

Redis 常用数据类型及其典型使用场景是什么?

高频 中等 第 3 / 36 题 更新于 2026/07/27
Redis数据结构缓存

简化版

五个核心类型:String(缓存、计数器、分布式锁)、Hash(存对象、字段级读写)、List(消息队列、时间线)、Set(去重、共同好友、标签)、ZSet(排行榜、延迟队列)。外加进阶的 Stream(消息流)、Bitmap(签到/在线状态)、HyperLogLog(海量去重计数)。选型的核心是按访问模式选,而不是把什么都塞进一个大 String。

详细版

类型底层/特点典型场景
String最基础,可存文本/数字/二进制缓存对象、计数器(incr)、分布式锁(setnx
Hashfield-value 的映射存对象且要单字段读写(改用户昵称不用读整个对象)
List双向链表,有序可重复简单消息队列(lpush+brpop)、最新动态列表
Set无序不重复去重、共同好友(sinter 交集)、标签、抽奖
ZSet有序集合,每个成员带 score排行榜(按分排序)、延迟队列(score=到期时间戳)
Stream消息流,支持消费者组需要消费进度/ACK的轻量消息队列
Bitmap用位存布尔签到、用户在线状态(1 亿用户仅 12MB)
HyperLogLog基数估算海量 UV 去重统计(近似值,误差 ~0.81%)

选型的关键是访问模式。比如存一个用户对象:如果总是整体读写,用 String 存 JSON 就行;如果经常只改其中一个字段,用 Hash 更好,能字段级操作、省序列化开销。

⚠️ 别把一个不断增长的大 JSON 塞进单个 String 然后每次整块读出来改一个字段再写回去——网络传输和序列化成本巨大,还没法做字段级并发控制。

完整版教学

一、String:不只是存字符串

String 是最容易被小看的类型。它能存的远不止文本:

  • 计数器incr/incrby原子的,天然适合浏览量、点赞数、限流计数,不用担心并发丢更新。
  • 分布式锁set key value NX EX 30——NX 保证只有一个客户端能设成功(抢到锁),EX 设过期防死锁。这是 Redis 锁的基础(严谨场景用 Redlock 或 Redisson)。
  • 缓存对象:存 JSON 序列化后的对象,简单直接。

String 的优势是简单和命令丰富,但不能因此把所有业务都塞成大 JSON。比如用户对象只有 2KB,并且总是整体读写,String 存 JSON 很合适;如果对象增长到 200KB,且每次只改一个昵称字段,整块读改写就会浪费网络和序列化成本。计数器场景则应优先用 INCRBY,因为它是 Redis 单命令原子执行。

INCRBY article:1001:view 1
SET user:1001 '{"name":"Tom","age":18}' EX 3600
SET lock:order:1 uuid NX PX 30000

二、ZSet 是「瑞士军刀」——重点掌握

ZSet(sorted set)是面试最爱问的,因为它用一个「成员唯一 + score 排序」的结构撬开了很多场景:

  • 排行榜zadd rank 100 userAzrevrange rank 0 9 直接拿 Top 10,实时排序,O(log n) 更新。
  • 延迟队列:score 存「任务到期时间戳」,用 zrangebyscore 捞出「已到期」的任务来执行——比轮询数据库高效得多。
  • 时间线:score 存时间戳,天然按时间排序。

它底层是跳表(skiplist)+ 哈希表:跳表保证有序和范围查询,哈希表保证 O(1) 按成员查 score。理解这点就明白它为什么既能排序又能快速定位。

排行榜可以这样理解:每个用户是 member,分数是 score,更新分数用 ZADD,查 Top 10 用 ZREVRANGE。延迟队列则把“执行时间戳”当 score,到点后用 ZRANGEBYSCORE 找出 score 小于当前时间的任务,再用 Lua 或原子删除避免重复消费。ZSet 的强项是“按分值排序 + 按范围取数据”,不是普通 key-value 缓存。

场景memberscore
游戏排行榜user_id分数
延迟队列task_id到期时间戳
时间线feed_id发布时间
热度榜item_id综合热度分

三、Set vs ZSet、List vs Stream:怎么区分

初学者常纠结相似类型,抓住关键差异:

  • Set vs ZSet:都去重,但 Set 无序、ZSet 按 score 有序。只要去重用 Set(省内存),要排序才用 ZSet。
  • List vs Stream 做消息队列:List 简单(lpush+brpop)但消息取走就没了、不支持多消费者组、不能确认重试;Stream 支持消费者组、ACK 确认、待处理列表(PEL),是更完整的轻量 MQ。要可靠消费用 Stream,图简单用 List。

如果只是抽奖去重、共同好友、标签集合,用 Set 更轻;如果要按分数排序或按时间范围取,用 ZSet。List 更像简单队列,两端推拉很方便;Stream 则更像轻量消息系统,有消息 ID、消费者组、ACK 和待处理列表。选型时不要先背类型名字,要先问访问模式:是否去重、是否排序、是否需要消费确认、是否需要字段级更新。

四、原子性:复合操作的坑

Redis 单个命令是原子的,但「先读再写」这种多步操作在并发下会丢更新:

# ❌ 非原子:get 和 set 之间,别的客户端可能也改了
v = GET counter
SET counter (v + 10)

# ✅ 用原生原子命令
INCRBY counter 10

如果逻辑没有对应的原生原子命令,用 Lua 脚本把多步操作打包(Redis 保证一个 Lua 脚本执行期间不被其他命令打断),或用事务(MULTI/EXEC,但注意它不支持中途逻辑判断)。

记忆点:能用原生原子命令(incr、setnx、lpush…)就别自己 get-then-set;复杂原子逻辑用 Lua 脚本。

用数字看丢更新:两个客户端同时 GET counter 都读到 100,各自加 10 后 SET counter 110,最终结果是 110,而不是 120。换成 INCRBY counter 10,两个命令会被 Redis 串行执行,最终就是 120。Redis 单线程保证单命令原子,不保证你在客户端拆开的多步逻辑自动原子。

五、Bitmap 和 HyperLogLog 的边界

Bitmap 适合大量布尔状态,例如签到、在线、是否活跃。1 亿用户如果每人 1 bit,理论上约 100000000 / 8 / 1024 / 1024 ≈ 11.9MB,这就是它适合海量布尔的原因。HyperLogLog 适合 UV 这类基数估算,占用空间小,但有约 0.81% 标准误差,不能用于需要精确去重名单的场景。

Bitmap:关心每个用户是否为 1/0
HyperLogLog:关心大概有多少不同用户,不关心具体是谁

六、类型选择要防 BigKey

Redis 数据类型强大,但每个类型都可能被用成 BigKey。Hash field 过多、List 无限堆积、Set/ZSet 元素爆炸,都会让单命令、持久化、复制和删除变重。设计时要给集合规模设上限,按时间、用户、业务维度拆分,并避免一次性全量读取。

例如排行榜只展示 Top 1000,就不要无限保留全量历史排名;消息队列如果用 List,消费者异常时要有积压监控和转移方案。类型选对只是第一步,容量边界和访问命令同样重要。

七、常见误区与追问

  • 误区:所有对象都适合用 String 存 JSON。 整体读写的小对象可以用 String,字段级频繁读写更适合 Hash,大对象整块读写会放大成本。
  • 误区:List 可以替代可靠消息队列。 List 能做简单阻塞队列,但缺少消费者组、ACK 和待处理消息管理,可靠消费更适合 Stream 或专业 MQ。
  • 误区:Set 和 ZSet 只是有没有排序的区别。 ZSet 维护 score 和有序结构,适合排行榜和范围查询,成本也比纯 Set 更高。
  • 误区:HyperLogLog 可以做精确去重。 它只做近似基数统计,不能返回具体元素集合,也不能用于精确对账。
  • 追问:并发计数为什么不能 GET 后 SET? 多客户端可能读到同一个旧值并覆盖彼此更新,应使用 INCRBY 或 Lua 脚本。
  • 追问:什么时候用 Bitmap? 当对象是海量布尔状态,如签到、在线、是否访问过,用位存储能显著节省内存。

八、加强记忆

Redis 类型选型要看访问模式:整存整取用 String,字段独立读写用 Hash,纯去重用 Set,有序去重和范围排名用 ZSet,简单队列用 List,可靠轻量消息用 Stream,海量布尔用 Bitmap,海量 UV 近似统计用 HyperLogLog。计数和锁尽量用原生命令,多步判断写入用 Lua 保证原子;同时给集合规模设边界,别把任何类型用成 BigKey。