面试 Redis 时,很多人会遇到这样的情况:网上收藏了一堆 85 问、100 题,翻来覆去背得滚瓜烂熟,可真到面试官面前,一句“你们项目里 Redis 怎么用的?”就把节奏打乱了。Redis 面试从来不是考零散的命令记忆,而是考你能不能从业务场景出发,把数据结构、分布式锁、缓存、集群这些知识串成一条线。本文围绕 Redis 面试最高频的三大主题——分布式锁、缓存、集群,从原理到实战帮大家理清主线,同时覆盖核心数据结构和持久化知识,尽量做到一篇文章形成完整面试闭环。
1. Redis 面试到底在考什么
1.1 Redis 的定位与核心价值
Redis 是一个基于内存的键值存储系统,但它的价值远远不只是“快”。它把常见的数据结构直接做成了服务端命令,比如字符串、哈希、列表、集合、有序集合,还有后来的 Stream、Bitmap、HyperLogLog、地理坐标等。这让开发者可以用较少代码实现分布式环境下的缓存、计数器、排行榜、消息队列等能力。
Redis 之所以能成为面试常客,是因为它几乎成为后端系统的标配组件。缓存是它最广为人知的用途,但分布式锁、分布式会话、接口限流、排行榜、Feed 流、实时统计等场景也经常选择 Redis。面试官问 Redis,本质上是在考察候选人是否理解“分布式环境下如何处理共享状态、怎么做高并发读写、怎么保证数据一致性”这一系列问题。
所以准备 Redis 面试,不能只背命令和数据结构,更应该把每个知识点还原到业务场景中去理解。比如面试官问“Redis 为什么快”,如果你只回答“内存操作、单线程”,就会漏掉 IO 多路复用、高效数据结构、避免上下文切换等关键点;同样,面试官问“分布式锁怎么实现”,如果只回答 setnx,那后面关于误删、过期续期、集群脑裂的连环追问就很难接住。
1.2 Redis 面试知识地图
结合近几年的高频面试反馈,Redis 面试常考板块基本稳定在以下五块:
| 板块 | 高频考点 | 面试官到底想听什么 |
|---|---|---|
| 数据结构 | 五种基本类型、底层编码、跳表、SDS | 能讲清楚使用场景和原理 |
| 持久化 | RDB、AOF、混合持久化、fork 阻塞 | 知道如何取舍,生产怎么配 |
| 分布式锁 | setnx 演进、过期时间、Redisson 看门狗、RedLock 争议 | 能指出坑并给出方案对比 |
| 缓存设计 | 穿透、击穿、雪崩、缓存一致性 | 能从并发场景推导出方案 |
| 集群高可用 | 主从复制、哨兵、Cluster、槽位与重定向 | 能画出整体架构并说明故障处理流程 |
本文按照这个地图展开,后半部分再给出一份高频速查表和答题建议。如果时间紧,建议优先掌握第 2 节的数据结构、第 4 节的分布式锁和第 5 节的缓存设计,这三块是面试中出现频率最高的。
2. Redis 核心数据结构与底层原理
2.1 五种基本类型的面试回答模板
先看最基础的:String、Hash、List、Set、ZSet。面试中经常出现的一种问法是“你在项目里用过哪些 Redis 数据类型,分别用在什么场景”。这里不要只说类型名,最好给出一个具体场景和命令。
| 类型 | 典型场景 | 核心命令 | 面试加分点 |
|---|---|---|---|
| String | 缓存、计数器、分布式 ID、限流计数 | set、get、incr、setnx、setex | 底层是 SDS,incr 是原子操作 |
| Hash | 对象缓存、购物车、用户资料 | hset、hget、hgetall、hscan | 比 String 存 JSON 更省内存,支持字段级更新 |
| List | 消息队列、时间轴、简单任务队列 | lpush、rpop、blpop、llen | 阻塞版本 blpop 可实现简易队列 |
| Set | 去重、共同好友、抽奖 | sadd、sismember、sinter、sdiff | 集合运算适合做社交关系计算 |
| ZSet | 排行榜、延时队列、限流 | zadd、zrangebyscore、zscore、zrevrank | 跳表实现,可排序还可按分数范围查询 |
回答时可以按“场景 + 数据结构 + 为什么选它”的结构来组织。例如排行榜,使用 ZSet 是因为它天然支持分数和排序,zadd 可以更新分数,zrevrange 可以快速取 Top N,并且插入和查询的时间复杂度相对可控。只背命令名称很难给面试官留下印象,但如果你能说出“排行榜用 ZSet,因为分数排序是它的核心能力”,效果会好很多。
2.2 底层编码与常见面试追问
五种类型只是对外暴露的接口,底层的编码方式会根据数据规模动态切换。面试中常被追问的有几个点:
String 底层为什么用 SDS(Simple Dynamic String)而不是 C 字符串?
因为 SDS 记录了长度,获取长度是 O(1);SDS 在扩容时做了预分配,减少内存重分配的次数;同时 SDS 是二进制安全的,可以存储图片、序列化对象等包含 \0 的数据。此外 C 字符串拼接容易造成缓冲区溢出,SDS 在 API 层面做了保护。ZSet 为什么用跳表而不是红黑树?
跳表实现简单,范围查询时和链表类似可以顺序遍历;红黑树也能做范围查询,但代码更复杂,并且需要维护更多指针。Redis 在内存和缓存友好的前提下选择了跳表,同时在字典中保存成员与分数的映射,保证 ZSCORE 等命令能 O(1) 完成。Set 和 Hash 在元素较少时用什么编码?
这是一个常见的版本敏感问题。较老版本使用 intset 和 ziplist 做小规模优化,较新版本逐步用 listpack 替换 ziplist。面试时可以说“Redis 会对小数据量的 key 使用紧凑编码以减少内存占用,数据量超过阈值后转换为哈希表或跳表等常规结构”,具体阈值需要以实际版本配置为准。
了解了底层原理,回答问题会明显更有底气,因为面试官很容易顺着“为什么选 ZSet”继续追问“底层怎么实现排序”。如果只停留在 API 使用层面,这一轮追问很容易暴露出准备不足。
3. Redis 持久化:RDB 与 AOF 的取舍
3.1 RDB 快照
RDB 是 Redis 在指定时间间隔内生成内存数据快照的文件。默认配置会按触发条件自动执行 bgsave,主进程通过 fork 一个子进程来完成快照写入。使用子进程的好处是主进程可以继续处理命令,减少对线上服务的影响。
RDB 的优点很明显:文件紧凑,适合做定时备份和灾难恢复;恢复速度比 AOF 快。但缺点也很致命:如果 Redis 在最后一次快照之后崩溃,这段时间的写操作会全部丢失。所以如果业务对数据丢失非常敏感,不能只依赖 RDB。
触发 RDB 的常见方式有 save、bgsave,以及配置中的 save m n 规则。注意 save 命令是同步阻塞的,生产环境基本不会手动使用;bgsave 会 fork 子进程,在写多或内存大的场景下依旧可能出现短暂阻塞,这是面试中经常追问的点。面试官想听的不是“RDB 是快照”,而是你知道它的触发方式、阻塞风险和取舍逻辑。
3.2 AOF 日志
AOF 以追加日志的方式记录每次写命令,恢复时重新执行日志即可。通过 appendfsync 配置控制刷盘策略:
| 配置 | 行为 | 丢数据风险 | 性能 |
|---|---|---|---|
| always | 每次写命令都 fsync 到磁盘 | 基本不丢 | 最差 |
| everysec | 每秒 fsync 一次 | 最多丢 1 秒数据 | 较好 |
| no | 由操作系统决定刷盘时机 | 可能丢较多 | 最好 |
生产环境常用 everysec,因为它在性能和可靠性之间比较均衡。AOF 也有一个需要关注的点:日志文件会不断增长,所以 Redis 支持 AOF 重写,也就是在后台生成一条精简后的日志来替代旧日志。重写过程中使用子进程,同时主进程的写命令会缓冲到 AOF 重写缓冲区,确保新老文件切换时数据不丢失。
3.3 混合持久化与现代生产实践
从 Redis 4.0 开始,可以开启混合持久化:在 AOF 文件头部先放一个 RDB 快照,后面再追加增量写命令。这样加载速度比纯 AOF 快,又比纯 RDB 丢数据少。开启方式是在 redis.conf 中配置 aof-use-rdb-preamble yes,具体配置名在不同版本中可能略有差异,需要以实际版本文档为准。
面试中的回答思路可以总结为:RDB 恢复快但丢数据多,AOF 安全但文件大,混合持久化是常见折中方案;生产环境应该同时开启 AOF 和 RDB,并定期手动备份,不要因为“内存数据库”就忽略持久化。这一节不算最难,但很容易被忽略,一旦被问到“Redis 重启后数据还在吗”,很多人会答不上来。
4. Redis 分布式锁:从 setnx 到 Redisson
4.1 为什么需要分布式锁
在多实例部署下,多个服务节点同时操作同一份共享资源,比如扣减库存、重复下单、任务调度,如果只靠本地锁,每个 JVM 进程内部各自锁住一部分,无法实现跨进程互斥。分布式锁的作用就是让多个进程只能有一个拿到锁,拿到锁的节点执行完业务后再释放锁。
Redis 实现分布式锁的思路是借助单个 Redis 节点的串行执行能力:谁先成功写入某个 key,谁就获得锁,结束后删除 key 释放锁。这个思路简单,但要真正用到生产环境,需要解决很多细节问题。面试官一般会从“你用过分布式锁吗”开始,一路追问到实现细节,这一节的内容基本就是完整的追问链路。
4.2 基础版本:SET NX EX
早期很多文章会用 setnx + expire 两条命令来实现锁,但这有严重问题:setnx 成功之后,如果 expire 设置失败或进程崩溃,锁会永远不释放。所以正确写法要使用一条命令同时设置过期时间和 NX 条件:
# 只有 key 不存在时才能设置成功,同时设置 30 秒过期时间 SET lock:order:1001 6f8a2d91-b0d4-4e1c-9c3a-2a6f1e5c9b01 NX EX 30这里的 value 要使用一个每次请求都不同的唯一 ID(比如 UUID)。这样做的原因是,释放锁时需要校验 value,防止当前线程把别的线程刚获取到的锁给删掉。如果只用固定的 “1” 作为 value,就会出现一个线程释放了另一个线程的锁这种典型事故。
释放锁的 Java 示例思路如下,需要先判断 value 是否一致,再删除,并且这两个步骤要保证原子性,所以推荐使用 Lua 脚本:
// 核心逻辑:先校验再删除,保证原子操作 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; // 通过 RedisTemplate 或 Jedis 执行该 Lua 脚本 // KEYS[1] = 锁的 key // ARGV[1] = 加锁时写入的 requestId如果不用 Lua 脚本,而是先 get 判断再删,那么在 get 和 del 之间一旦发生时间片切换,依然存在误删风险。这一点面试中经常考察,一定要能主动讲出来。很多候选人能说出 set nx ex,却说不清为什么释放锁要用 Lua,这就是扣分点。
4.3 过期时间与看门狗机制
加锁时必须设置过期时间,否则节点宕机后锁永远不会释放。但过期时间怎么定?如果业务执行时间超过过期时间,锁被自动释放,其他线程进入临界区,就会导致并发冲突;如果过期时间设得太长,一旦持有锁的节点异常,其他节点又需要等很久。
Redisson 提供了一个经典方案:可重入锁加看门狗续期。加锁后如果业务没有执行完,后台会每隔一段时间自动给锁续期,默认情况下锁的 watchdog 超时时间是 30 秒,续期可以保证业务未完成时锁不会提前释放。使用 Redisson 的代码大致是这样:
RLock lock = redissonClient.getLock("lock:order:1001"); boolean locked = false; try { // 无 leaseTime 参数时,Redisson 默认启用看门狗自动续期 // waitTime 表示获取锁的最大等待时间 locked = lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { // 获取锁失败,做降级或抛出异常 } // 业务逻辑 } finally { if (locked) { lock.unlock(); } }需要理解的是,看门狗续期并不是无限续期,它是尽最大努力保证“业务没做完锁还在”。如果持有锁的节点宕机,续期线程也会终止,锁最终会因过期时间自动释放,不会死锁。这里建议深入看一下 Redisson 的看门狗源码,面试时能说出实现原理,会明显拉开和其他候选人的差距。
4.4 可重入、RedLock 优缺点与选型对比
可重入是指同一个线程在已经持有锁的情况下,可以再次获取同一把锁。Redisson 的 RLock 使用 Hash 结构记录线程标识和重入次数,重入时加一,释放时减一,减到零才真正删除。如果只用 setnx 自己实现,默认不可重入,需要自己维护线程标识和计数,复杂度会明显上升。
关于 RedLock,面试中常被问到。RedLock 的思想是向多个相互独立的 Redis 节点同时申请锁,超过半数节点成功就认为加锁成功,以此来降低单点故障影响。但分布式领域有知名专家对 RedLock 提出过质疑,主要问题包括:主从切换可能导致锁丢失;客户端 GC 暂停可能导致锁过期而其他节点已进入临界区;节点时钟跳跃会影响过期时间判断。对于绝大多数业务来说,更务实的方案是使用单实例或哨兵模式下的 Redis 分布式锁,并在业务层做好幂等和兜底;如果对锁的可靠性要求极高,可以考虑 ZooKeeper 或 etcd 实现的分布式锁。
选型对比时可以参考下表:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis SETNX | 实现简单、性能高 | 存在锁丢失和过期续期问题 | 大多数互联网业务缓存型互斥 |
| Redisson | 开箱即用、有看门狗和可重入 | 依赖 Redis 自身可靠性 | 中大型项目常用方案 |
| ZooKeeper 临时顺序节点 | 强一致、无锁丢失问题 | 性能较低、运维成本高 | 对锁可靠性要求高的金融/分布式任务 |
| etcd | 强一致、lease 续期 | 引入新组件 | 云原生场景常见 |
回答分布式锁题目时,最好的节奏是先画出一条演进线:原生 setnx