做后端和中间件开发这些年,Redis 基本是绕不开的组件。面试问,简历写,业务里天天用,但真正把数据类型吃透的人,说实话不多。很多人背了八股“五种基本类型:String、Hash、List、Set、ZSet”,真到线上缓存设计的时候,照样把对象整个 JSON 塞 String,把排行榜用 List 硬排,把需要去重的数据放 Set 里结果内存暴涨——这些都是我没少踩过的坑。这篇就围绕 Redis 的数据类型,把原理、适用场景、命令细节、坑点和排查思路完整过一遍。不管你是刚接触 Redis 的入门者,还是用了两三年想系统梳理的开发者,这篇都值得认真读一遍,尤其是后半部分的选型判断和问题排查,是常规文档里不会写的东西。
1. 先别急着敲命令:Redis 数据类型到底在解决什么问题
1.1 六种数据类型一张表先看懂全局
Redis 官方把数据类型分成基础五类和三种扩展结构(BitMap、HyperLogLog、Geo),但日常开发中你真正接触的,基本是 String、Hash、List、Set、ZSet 这五种,再加上 Redis 5.0 引入的 Stream。先用一张表把这六类的核心特性梳理清楚,后面再逐个展开讲原理和命令。
| 数据类型 | 底层主要实现 | 单 key 容量上限 | 典型使用场景 | 一句话理解 |
|---|---|---|---|---|
| String | SDS(简单动态字符串)、int、embstr | 512 MB | 缓存、计数器、分布式锁、Session | 一个 key 一个值,最朴素的键值对 |
| Hash | listpack(或 ziplist)、hashtable | 字段数无上限,单值 512 MB | 对象存储、购物车、配置项聚合 | 一个 key 里塞了一个小 map |
| List | quicklist | 元素数量无上限 | 消息队列、时间线、最新列表 | 一个有序的双向链表 |
| Set | intset、hashtable | 元素数量无上限 | 去重、共同好友、随机抽奖 | 无序且元素唯一 |
| ZSet | listpack、skiplist + hashtable | 元素数量无上限 | 排行榜、延时队列、限流窗口 | 每个元素带一个权重分数,按分数排序 |
| Stream | listpack + rax(基数树) | 消息数无上限 | 消息队列、事件总线 | 带消费组功能的消息流 |
这里有个细节值得注意:Hash、ZSet 底层在小数据量时会用紧凑结构(ziplist/listpack)节省内存,数据量增长到阈值后会切换成哈希表或跳表。也就是说,同一个类型在不同数据规模下,内部结构和性能特征是截然不同的。这也是很多人在面试中被问“Redis 的 ziplist 什么时候会变成 hashtable”的原因——这直接关系到内存和查询效率的取舍。
1.2 为什么 Redis 要有五种类型,而不是一个万能字典
最常见的误解,是觉得“反正都是 key-value,我统一用 String 存 JSON 不就完了”。这个思路在数据量小、访问量低的时候确实没问题,但一旦业务体量上来,问题就暴露了:
第一,网络开销和序列化开销呈线性放大。比如存一个用户对象,有用户名、年龄、积分、等级四个字段。用 String 存整个 JSON,每次只改积分也要把整个对象反序列化、改完再序列化写回;而 Hash 可以直接HINCRBY user:1001 score 10,一次命令搞定。假设每秒操作一万次,前者多出来的 CPU 和网络耗时是实打实的成本。
第二,某些逻辑用 String 根本无法原子实现。比如“给商品库存减一”,String 要三步:GET 出来、减一、SET 回去,中间任何一步出现并发问题都会超卖。但用DECR stock一条命令就完成,Redis 单线程执行保证原子性。
第三,不同类型对应的底层结构,决定了它能支持的高阶操作。Set 的交并集运算、ZSet 的范围查询、List 的阻塞弹出,这些都是数据结构天然带来的能力。如果只用 String,你得在应用层用 Lua 脚本或者多次查询去模拟,又慢又容易出错。
所以 Redis 设计多种类型,本质上是在帮你把“数据组织方式”和“操作语义”下沉到服务端。你选对了类型,等于让 Redis 这个单线程引擎替你把最脏最累的活干了。
2. 五种基础类型拆开讲:原理、命令与踩坑点
2.1 String:最常用,也最容易用错
String 是所有类型的基础,也是最容易被人忽视的一个。它的存储结构不是 C 语言传统的char*数组,而是 Redis 自己实现的 SDS(Simple Dynamic String)。SDS 在结构里记录了已用长度和可用长度,这样获取字符串长度是 O(1),而且因为以长度为准而不是以\0结尾,二进制安全——你可以往里面存图片、序列化对象,哪怕是带空字符的数据也不会被截断。
实际操作中,String 的核心命令就这几个:SET、GET、MSET、MGET、INCR、DECR、INCRBY、SETNX、SETEX。其中SETNX(set if not exists)是实现分布式锁的基石,INCR系列用于计数器。
# 基础读写 SET user:1001:name "张三" GET user:1001:name # 批量读写,减少 RTT MSET user:1001:age 20 user:1002:age 22 MGET user:1001:age user:1002:age # 计数器 INCR page:view:2025:04:01 INCRBY user:1001:score 10 # 带过期时间的设置,常用于缓存 SETEX token:abc123 3600 "session-data" # 不存在才设置,分布式锁核心 SET lock:order:1001 1 NX EX 10分布式锁这块多说一句。很多人还在用SETNX加EXPIRE两条命令实现,这是有问题的,两条命令之间不是原子的,如果 SETNX 成功但 EXPIRE 没执行,锁就永远不会过期,直接死锁。正确姿势是上面写的SET lock:order:1001 1 NX EX 10,在一条命令里同时指定 NX 和过期时间。
String 常见的坑,一个是用字符数组存非文本数据导致乱码,这就是没理解 SDS 的二进制安全特性;另一个是滥用 String 存对象,这点在第三章选型部分会重点展开。
2.2 Hash:对象存储的“正确打开方式”
Hash 在 Redis 里的语义是一个 field-value 映射表,最适合表示一个对象。比如用户对象有 id、name、age 三个属性,你会有两种选择:
# 方案一:三个 String key SET user:1001:id 1001 SET user:1001:name "张三" SET user:1001:age 20 # 方案二:一个 Hash key HSET user:1001 id 1001 name "张三" age 20方案的好处一对比就出来了:方案二只占一个 key,整体数据集中存储,内存碎片更少;要改其中一个字段只需要一次HSET,不用整对象读写;而且可以把多个属性打包到同一个 key 里,减少 key 数量,降低 Redis 的内存管理开销。
Hash 底层有两套实现。数据量小的时候用 listpack(旧版本是 ziplist),它是一种连续内存块结构,把所有 field 和 value 挨个拼接在一起,省内存但查询是 O(n)。当 field 数量超过hash-max-listpack-entries(默认 128)或者单个 value 长度超过hash-max-listpack-value(默认 64 字节)时,自动转为 hashtable,查询变 O(1) 但额外内存开销增加。
这里有一个非常容易出现的问题:小 Hash 的纯内存结构导致“大 key”隐患。你看着每个 Hash 没多大,但如果一个 key 的 field 特别多,比如把一百万用户的每个属性都塞进一个 Hash,这个 key 就是典型的大 key。后面讲排查时会详细说。
常用命令:HSET、HGET、HGETALL、HMGET、HINCRBY、HDEL、HLEN、HEXISTS。
HSET user:1001 name "张三" age 20 HGET user:1001 name HMGET user:1001 name age HINCRBY user:1001 score 5 HGETALL user:1001特别注意HGETALL在 field 特别多的时候要把所有数据拉到客户端,会阻塞 Redis 吗?不会阻塞 Redis 服务端,但会占用大量网络带宽和客户端内存。我见过有人用HGETALL拉一个有几百万 field 的 Hash,直接导致客户端 OOM。对大数据量的 Hash,建议用HSCAN分批次迭代读取。
2.3 List:左右压入、范围查询,但别把它当万能队列
List 在 Redis 中是一个按插入顺序排列的字符串列表,底层实现是 quicklist,它是双向链表和压缩列表的组合体。每个 quicklist 节点是一个压缩列表,多个压缩列表通过双向指针串联。这样设计的目的是在内存占用和读写性能之间找平衡:链表节点多则内存碎片多,压缩列表太长则插入删除慢。
List 的核心命令围绕左端(头部)和右端(尾部)操作展开:
# 右侧压入,左侧弹出——先进先出队列 RPUSH msg:queue "hello" LPOP msg:queue # 左侧压入,左侧弹出——栈结构 LPUSH username:stack "alice" LPOP username:stack # 范围查询 LRANGE msg:queue 0 -1 # 阻塞弹出,队列为空时等待,直到超时 BRPOP msg:queue 10 # 按索引设置和插入 LSET msg:queue 0 "new-value" LINSERT msg:queue BEFORE "hello" "inserted"面试里经常听到“用 Redis List 做消息队列”,这确实是最常见的用法,但它和专业的消息队列(RocketMQ、Kafka)差别很大。List 做队列有一个致命短板:不支持多消费者订阅同一个消息。LPOP和BRPOP是竞争消费,一条消息被一个人拿走,别人就消费不到了,这不是发布订阅,而是任务队列。
如果业务场景真的是“一个消息要发给多个消费者”,别用 List,用 Pub/Sub 或者 Stream。另外一个经常踩的坑是拿 List 当数据库用,把所有历史记录无限地往里塞。List 一旦积累到百万级别,LRANGE的操作效率会明显下降,而且内存占用居高不下。做时间线列表一定要做分页 + 限制最大长度,甚至定期裁剪:
# 只保留最近 500 条 LTRIM msg:timeline 0 4992.4 Set:去重、随机、集合运算,比你想的更能干
Set 的特征就两个字:无序、唯一。底层有两种编码,当集合元素全是整数且数量不大时用 intset(整数集合),它是排好序的整数数组,查找用二分,内存极省;一旦元素不是整数或者数量超过set-max-intset-entries(默认 512),就转成 hashtable。
Set 的典型命令和场景:
# 添加、去重 SADD online:user 1001 1002 1003 SISMEMBER online:user 1001 # 1 表示存在 # 集合运算:交集、并集、差集 SINTER tag:java tag:redis # 同时打了两个标签的用户 SUNION tag:java tag:redis SDIFF tag:java tag:redis # 打了 java 但没打 redis 的用户 # 随机弹出——抽奖场景 SPOP lottery:pool # 中奖后移出,保证不重复中奖 SRANDMEMBER lottery:pool 3 # 统计数量 SCARD online:user业务里 Set 最常见的使用场景有三个:
一是标签系统。给文章打标签,给用户打标签,用 Set 存储后可以直接做交集运算,比如“想看 Java 和 Redis 都感兴趣的文章”,就是一个SINTER的事。这比在数据库里用IN子查询高效得多。
二是在线状态/去重统计。用户上线往 Set 里SADD,下线SREM,SCARD就是当前在线人数,SISMEMBER判断某用户是否在线。数据量大了以后可以拆成多个 Set 分片,避免单个 key 过大。
三是抽奖去重。SPOP是随机弹出一个元素并移除,天然保证不会重复中奖。我之前做过一个抽奖组件,只用了两条命令就完成了“抽取一人并从奖池移除”的操作,应用逻辑只需要处理奖品分发。
Set 容易踩的坑是错误地拿 Set 做排行榜。Set 能去重,但它没有顺序概念,不能按分数排序。如果你想“按得分从高到低展示前 100 名”,应该用 ZSet,而不是把用户 Set 出来再在应用层排序——后者在大数据量下性能惨不忍睹。
2.5 ZSet:跳表、分数和排行榜的黄金搭档
ZSet 是 Redis 里最有“技术含量”的类型,每个元素关联一个 double 类型的分数,集合按分数从小到大排序。底层由跳表(skiplist)和哈希表共同实现,哈希表负责 O(1) 地根据元素查分数,跳表负责按分数有序地遍历和范围查询。
跳表这个结构值得多说一句。它本质上是一个多层链表,每一层都是下一层的“快速通道”,查找时从最高层开始跳跃,平均 O(log n) 的复杂度。为什么 Redis 不用平衡树而用跳表?因为跳表实现更简单,范围查询(比如查第 10 到第 20 名)非常自然,而且在并发环境下不需要复杂的旋转操作。这是面试高频点,能答清楚跳表的层数随机生成、查找过程,会加分不少。
ZSet 的核心命令:
# 添加或更新分数 ZADD leaderboard:game1 100 "player_a" 88 "player_b" # 分数更新 ZINCRBY leaderboard:game1 12 "player_a" # 排名查询,从 0 开始,低分在前 ZRANK leaderboard:game1 "player_b" ZREVRANK leaderboard:game1 "player_b" # 按排名范围获取(低分到高分) ZRANGE leaderboard:game1 0 9 # 按排名倒序获取,榜单场景用这个 ZREVRANGE leaderboard:game1 0 9 WITHSCORES # 按分数范围获取 ZRANGEBYSCORE leaderboard:game1 80 100 # 统计分数区间内的人数 ZCOUNT leaderboard:game1 80 100 # 删除 ZREM leaderboard:game1 "player_c"ZSet 的场景比很多人以为的要多:
- 排行榜:最经典,写入分数,倒序取前 N,天然有序。
- 延时队列:把任务 ID 作为元素,执行时间戳作为分数,用一个后台线程循环
ZRANGEBYSCORE+ZREM取到期任务。 - 滑动窗口限流:用当前时间戳作为分数,往 ZSet 里
ZADD,然后删除窗口外的旧记录,ZCOUNT统计窗口内的请求数。
ZSet 的坑主要有两个。一是分数是 double 类型,注意精度问题。比如ZADD key 0.1 "a"和ZADD key 0.2 "b",在极端情况下可能因浮点精度导致排序不符合预期。对关键业务,尽量把分数设计为整数,比如时间戳毫秒值。二是不要对同一个 ZSet 频繁 ZADD 更新超大成员的分数,跳表的更新涉及层级指针调整,虽然复杂度是 O(log n),但高频操作仍然会有性能开销。可以合并更新,用管道或者 Lua 脚本批量提交。
3. 类型选型的实操判断:从业务到 Redis 的映射方法
3.1 场景映射:我的业务到底该用哪个类型
很多开发者拿到需求后,第一个问题就是“我该用哪种类型”。我的经验是不要背规则,而是反问自己三个问题:
第一个问题:数据是单值还是对象?单值(一个 key 对应一个值)用 String;对象(一个 key 对应多个字段)优先 Hash。比如用户 Session、商品详情、配置项集合,都是典型的 Hash 场景。
第二个问题:数据有没有顺序要求?无顺序且需要去重用 Set;有顺序且按插入时间排列用 List;有顺序且按分数排序用 ZSet。比如“最新文章列表”,用 List 左压入然后LRANGE 0 9就可以;比如“比分排行榜”,必须 ZSet。
第三个问题:操作的模式是什么?只读改单个字段用 Hash;需要范围查询用 ZSet;需要阻塞队列用 List;需要统计去重用 Set;需要计数器用 String。
下面这张表是我做业务梳理时经常用的对照表,可以当作选型参考:
| 业务场景 | 推荐类型 | 原因 | 不推荐的做法 |
|---|---|---|---|
| 商品详情缓存(多字段) | Hash | 可按需更新单个字段,节省流量 | String 存整个 JSON,全量读写 |
| 全局计数器(点赞、库存) | String + INCR | 原子自增自减,无并发问题 | 应用层 GET + SET,有竞态 |
| 排行榜 | ZSet | 分数排序 + 范围查询原生支持 | List + 应用层排序 |
| 去重集合(标签、在线用户) | Set | 唯一性由 Redis 保证 | String + 程序去重 |
| 异步任务队列 | List + BRPOP | 阻塞弹出,天然先进先出 | 用 ZSet 或 String 模拟队列 |
| 分布式锁 | String + NX + EX | 单命令原子设置锁和过期时间 | SETNX + EXPIRE 两步操作 |
| 延时任务 | ZSet | 分数存时间戳,轮询取到期任务 | 写一个定时任务扫数据库 |
| 消息多播(发布订阅) | Pub/Sub 或 Stream | 支持一消息多消费者 | List 的 LPOP 只能竞争消费 |
特别提醒一下,场景不是一成不变的。比如你用 List 做了消息队列,后来发现需要消费确认、需要消费者组、需要回溯消息,就该迁移到 Stream。Redis 5.0 引入 Stream 很大程度上就是为了弥补 List 做队列的不足。在选型时留一点“未来可迁移”的余地,别把方案焊死。
3.2 key 命名与序列化:很多线上事故的第一步
类型选对了,接着就是 key 的命名和值的序列化问题。这两点看似琐碎,实际踩坑率极高,而且一出事就是全局性的。
先说 key 命名。线上 Redis 集群有几千上万个 key,如果没有统一命名规范,排查问题就像大海捞针。我的习惯是“业务:实体:ID:属性”的分层模式,比如:
user:1001:profile order:20250401:list message:queue:order冒号分隔的好处是,在 Redis Desktop Manager 这类可视化工具里,key 会自动按前缀分组展示,一眼就能看出某个业务模块的 key 分布。另外,key 不要随便搞成无意义的随机串(除非是 Session/Token),否则后期做数据清理和容量规划都会很痛苦。
再说序列化。这一块和数据类型强相关,特别是用 Spring Data Redis(RedisTemplate)的时候,很多人会遇到一个经典问题:往 Redis 里存了一个对象,查询出来的值变成乱码,或者类型对不上。原因通常是序列化器没配对:
- String 类型的存值,用
StringRedisSerializer即可,读出来是纯字符串。 - Hash 的 value 如果是 JSON,用
GenericJackson2JsonRedisSerializer序列化。 - 如果你用默认的 JdkSerializationRedisSerializer 存 Java 对象,key 和 value 会出现一堆
\xAC\xED\x00开头的内容,这就是 Java 序列化的二进制头,丑不说,还会浪费大量内存,而且跨语言读取基本不可能。
解决思路是:key 用 StringRedisSerializer,value 按业务选 JSON 或二进制序列化器。我在项目里通常配置为 key 使用 String,value 使用 Jackson JSON。要注意,如果你把数字类型(比如 Integer)直接存进 Redis,取出后可能变成 Integer 或者 Long,具体看序列化器。这点在热词“java数据类型”相关的坑里也经常出现——大多数人以为存进去是什么类型,取出来还是什么类型,事实上序列化器会很大程度影响这一点。
下面是一个简单配置示例:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key 用 String template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // value 用 JSON GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }这里还有一个隐性坑:GenericJackson2JsonRedisSerializer 序列化后,对象会多出一个@class字段,用来记录类型信息,这样反序列化时才能还原成原来的 Java 类。但是这也会让存储的体积变大。如果对体积敏感,可以考虑用 Jackson 的默认 TypeInfo 策略,或者自定义序列化器。总之,没有万能的序列化方案,每个方案都有 trade-off。
4. 常见问题与排查技巧实录
4.1 大 key 和热点 key:Redis 数据类型的隐形杀手
大 key(Big Key)是指单个 key 存储的数据量过大或者成员数量过多。不同数据类型产生大 key 的方式不一样:
- String 类型:value 超过 10 KB 就要警惕,比如一个超长 JSON。
- Hash 类型:field 数量超过 10000,比如把所有用户积分塞一个 Hash。
- List 类型:元素数量超过 10000,或者单元素体积过大,比如未经裁剪的操作日志。
- Set/ZSet:成员数量巨大,或者单个成员是超长字符串。
大 key 的危害是实打实的:删除大 key 会阻塞 Redis 主线程,因为 DEL 是一个同步操作;读取大 key 会占用网络带宽;内存碎片高企。我遇到过最典型的一次事故:一个 List 存了 80 万条日志,项目上线半年后执行DEL进行清理,Redis 直接阻塞了 3 秒多,全靠主从切换才没影响线上。后续排查才发现这个隐患一直潜伏着。
排查大 key 的方法很简单,用--bigkeys参数扫描:
redis-cli --bigkeys它会分别统计每种类型里最大的 key 和数量最多的 key。发现大 key 后的处理方式:String 类型超过阈值就压缩、拆分或者丢到其他存储;Hash、Set、ZSet 这类集合型大 key 按哈希分片成多个小 key,比如user:1001:part1、user:1001:part2。
热点 key(Hot Key)同样值得注意。它指某个 key 在极短时间内被超高频率访问,导致单个 Redis 实例 CPU 飙升、网络打满。热点 key 和数据类型的操作方式关系很大:比如一个 ZSet 排行榜的首页,所有流量都打到同一个 key 上。缓解手段常见的有:加本地缓存(Caffeine)、对热点 key 做副本拆分(key 后加随机后缀分散读流量)、给 key 设置合理的过期时间避免所谓“缓存击穿”。
4.2 内存统计和过期策略:类型不同,内存表现差很多
每种类型的内存占用模式差异很大,这决定了你该如何预估容量和设置过期。
String 类型最简单直接,value 多大占多大内存。Hash 在小数据量时用紧凑结构,一个字段的 Hash 可能几十字节,转成 hashtable 后固定开销会明显增加。ZSet 因为既有跳表又有哈希表,是内存开销最大的类型,一个成员大约要额外几十字节的索引开销。Set 如果是整数集合,非常省内存,但变成 hashtable 后每个元素多出一个哈希表 entry 的开销。
所以ZSet 不能无脑用。如果一个排行榜只要显示前 100 名,但入围数据有几百万,用 ZSet 存储全部数据会浪费大量内存。合理做法是只把有资格争夺前 100 名的人放进 ZSet,后面的人放到数据库,等分数足够再补录。这就是基于数据结构内存特性的“分治”。
过期策略也是高频问题。Redis 过期的机制是惰性删除 + 定期删除结合:读取 key 时发现已过期就删除;同时后台每秒抽样几次,随机挑一些设置了过期时间的 key 判断是否过期并删除。这带来的风险是过期 key 不会立刻消失,如果大量 key 在同一时间点过期,会导致瞬间的内存占用峰值和延迟抖动。所以做缓存时要给过期时间加一个随机扰动,比如:
SETEX cache:user:1001 3600 "data" # 不推荐 SET cache:user:1001 "data" EX 3600 # 同样不推荐 # 推荐:过期时间加随机值,避免雪崩 SET cache:user:1001 "data" EX $((3600 + RANDOM % 300))这一条经验在应对“缓存雪崩”时极其有效,比任何高级组件都管用。
4.3 可视化客户端与类型识别:别只靠命令行
排查问题时,除了redis-cli,图形化客户端能大幅提升效率。普通用户和开发者最常用的有两个:
| 工具 | 是否开源 | 跨平台 | 主要特点 |
|---|---|---|---|
| Another Redis Desktop Manager | 开源免费 | Windows/macOS/Linux | 界面简洁,支持 key 模糊搜索、分页查看、类型图标显示 |
| Redis Desktop Manager | 有免费版/付费版 | 跨平台 | 老牌工具,功能全,但新版部分功能收费 |
用这类工具,你可以直接看到每个 key 的类型图标(String、Hash、List、Set、ZSet 都有不同图标),点击 Hash 能看到所有 field 和 value,点击 ZSet 能看到带分数的成员列表。对于快速定位“某个 key 到底是什么类型”非常直观。命令行里判断类型的等价操作是:
TYPE user:1001 # 输出 hash / string / list / set / zset另外,用OBJECT ENCODING可以查看 key 的底层编码:
OBJECT ENCODING user:1001 # 输出 listpack / hashtable / intset / skiplist / embstr / raw这个命令特别适合验证我前面讲的“数据量小用紧凑结构、大则转哈希表/跳表”的机制。你可以在一个 ZSet 先插入 10 个成员,再插入 200 个成员,分别执行OBJECT ENCODING,会看到编码从 listpack 变成 skiplist。亲眼验证一次,比背十遍八股都有用。
5. 关于这五种类型的最后几个提醒
前面章节把五种类型的原理、命令、选型和排查讲完了,最后再补几个我认为同样重要的视角,包括面试视角和实际使用的习惯性建议。
5.1 高频面试题:数据类型方向的核心考点
我自己面试候选人的时候,数据类型方向的题不会问“Redis 有哪几种类型”这种背书的,而是会问到这几个层次:
- String 和 Hash 存储对象的区别,什么场景选哪个——考察的是对网络开销和操作粒度的理解。
- List 做消息队列和专业的 MQ 有什么差别——考察的是对消息语义的理解,竞争消费 vs 发布订阅。
- ZSet 底层为什么用跳表而不用平衡树——考察的是对数据结构取舍的认知。
- 大 key 的危害和解决方案——考察的是线上实战经验。
如果这些问题都能讲清楚,而且能结合自己的业务场景举例,那这个候选人对 Redis 绝对不是浮于表面的。反过来,如果你是在准备面试,不要只背“五种类型叫什么”,要把每种类型的底层结构、典型命令、适用场景、坑点梳理成一条线,面试时按这条线讲,远远好过零散的知识点。
5.2 扩展类型和后续学习方向
除了五种基础类型,Redis 还提供了 BitMap(基于 String 的位操作)、HyperLogLog(基数统计)、Geo(地理位置)。这些扩展类型在特定场景下非常高效:BitMap 做用户签到统计,一个用户一年只占 46 字节;HyperLogLog 做 UV 统计,十几 KB 就能统计几百万独立访客;Geo 做附近的人、门店距离排序,底层是 ZSet 的变种实现。
学习路线上的建议:先把这五种基础类型的命令全部过一遍,然后用真实业务场景各做一个 demo;之后再深入研究底层编码的转换条件,了解redis.conf里有哪几个参数控制转换阈值;最后再看源码或者至少看底层结构的设计讲解。不要一上来就啃源码,先做到“看到业务能快速反应出用哪个类型”,这比什么理论都重要。
说到底,Redis 的数据类型不是面试题,而是你在生产环境里每天都在用的一套工具箱。理解每个类型背后的数据结构和设计意图,你在做技术选型、性能优化、问题排查时,都会更有底气。