Redis 作为后端开发面试中的高频考点,几乎每一次 Java、Go、Python、Node.js 岗位面试都会涉及。它不仅是缓存组件,更在分布式锁、消息队列、排行榜、计数器、限流等场景中发挥着重要作用。本文精选了 21 道 Redis 最常问的面试题,覆盖基础概念、底层数据结构、持久化、高可用、缓存一致性、分布式锁等核心方向。
学习路径:定位与性能来源 → 数据类型与底层实现 → 持久化与高可用 → 缓存一致性与分布式锁。
第一部分:基础篇
1. 什么是 Redis?它有哪些优缺点?
Redis(Remote Dictionary Server)是一个基于内存的高性能键值存储系统,由 Salvatore Sanfilippo 使用 C 语言编写,支持字符串、哈希、列表、集合、有序集合等数据结构,并提供持久化、主从复制、哨兵、集群等能力。
核心定位:以内存为主要存储介质、以单线程事件循环为核心工作模型的远程数据结构服务器。它不是单纯的缓存,而是可以承担数据存储职责的内存数据库。
优点:
性能极高:基于内存读写,单机 QPS 可达 10 万级别。
数据结构丰富:list、hash、set、zset、bitmap、hyperloglog、geo、stream 等。
功能全面:持久化、过期策略、发布订阅、事务、Lua 脚本、分布式锁等。
生态成熟:几乎所有主流语言都有客户端。
高可用易扩展:主从复制、哨兵、集群。
缺点:
内存成本高:数据全部放内存,大容量存储成本远高于磁盘数据库。
数据一致性较弱:持久化是异步的,极端情况可能丢失最近数据。
单线程瓶颈:6.0 之前单个耗时命令会阻塞整个实例。
不支持复杂查询:无法进行多条件复杂查询和事务级强一致。
一句话总结:Redis 适合高并发、低延迟、数据量可控的场景,不适合作为海量数据的主存储。
2. Redis 为什么这么快?
不能只说「因为它是内存数据库」,要从多个维度展开:
基于内存存储:内存随机访问是纳秒级,磁盘是毫秒级,相差几个数量级。
高效的数据结构:SDS、跳跃表、压缩列表等,查询复杂度经过精心优化。
单线程事件驱动模型:避免多线程切换和锁竞争,命令执行不涉及 CPU 密集计算。
IO 多路复用:封装 epoll、kqueue、select,一个线程监听成百上千个 socket。
高效的通信协议:自定义 RESP 协议,解析简单高效。
特别说明:Redis 6.0 引入的多线程只针对网络 IO 读写,命令执行仍然是单线程。
3. Redis 有哪些数据类型?各自的适用场景是什么?
| 数据类型 | 特点 | 典型场景 |
|---|---|---|
| String | 最基本类型,值可以是字符串、整数、浮点数,最大 512MB | 缓存对象、计数器、分布式锁、Session 共享 |
| Hash | 键值对集合,适合存储对象属性 | 用户信息、购物车、商品详情 |
| List | 有序字符串链表,支持两端操作 | 消息队列、最新动态、时间线 |
| Set | 无序且不重复,支持交并差运算 | 共同好友、标签、抽奖去重 |
| ZSet | 有序集合,每个元素带分数排序 | 排行榜、延迟队列、按时间排序列表 |
进阶类型:Bitmap(签到统计、在线状态)、HyperLogLog(UV 统计,有误差)、GEO(附近的人)、Stream(可靠消息队列)、Bitfield(位域操作)。
计数器示例:
shell
127.0.0.1:6379> SET article:1001:views 0 OK 127.0.0.1:6379> INCR article:1001:views (integer) 1 127.0.0.1:6379> INCRBY article:1001:views 10 (integer) 11 127.0.0.1:6379> GET article:1001:views "11"
排行榜示例:
shell
127.0.0.1:6379> ZADD rank 100 user:a 95 user:b 120 user:c (integer) 3 127.0.0.1:6379> ZREVRANGE rank 0 -1 WITHSCORES 1) "user:c" 2) "120" 3) "user:a" 4) "100" 5) "user:b" 6) "95"
4. Redis 底层数据结构有哪些?
Redis 对外类型和底层实现之间有一个「编码」的映射关系,同一对外类型在不同条件下可能使用不同的底层结构。
| 底层结构 | 特点 |
|---|---|
| SDS(简单动态字符串) | O(1) 获取长度,空间预分配 + 惰性释放,String 默认实现 |
| 双向链表(LinkedList) | 插入删除快,指针开销大,早期 List 实现 |
| 压缩列表(ZipList) | 内存连续、占用小,插入删除可能连锁更新 |
| 快速列表(QuickList) | 多个压缩列表 + 双向链表,List 默认实现 |
| 哈希表(Dict) | 链式哈希解决冲突,渐进式 rehash,Hash 和全局键空间核心 |
| 整数集合(IntSet) | 只存整数且元素少时 Set 的编码 |
| 跳跃表(SkipList) | 多层有序链表,平均 O(logN),ZSet 主要实现 |
| ListPack | Redis 7.0 引入,替代 ZipList 解决连锁更新 |
查看编码:
shell
127.0.0.1:6379> SET num 12345 OK 127.0.0.1:6379> OBJECT ENCODING num "int" 127.0.0.1:6379> SET str "hello redis" OK 127.0.0.1:6379> OBJECT ENCODING str "embstr" 127.0.0.1:6379> SET longstr "这是一段超过 44 字节的字符串..." OK 127.0.0.1:6379> OBJECT ENCODING longstr "raw"
理解意义:数据量或元素大小变化时,编码会转换。例如 Hash 元素数量超阈值后从压缩列表转换为哈希表。
5. Redis 的过期键删除策略是什么?
Redis 结合三种策略:
惰性删除(Lazy Expire):客户端访问键时检查是否过期,过期则删除。对 CPU 友好,但过期键无人访问时长期占用内存。
定期删除(Active Expire Cycle):周期性主动扫描,随机抽取部分过期键检查,删除已过期的。过期键比例超过 25% 则继续循环。
内存淘汰机制兜底:内存达到 maxmemory 上限时,按淘汰策略清理。
验证惰性删除行为:
shell
127.0.0.1:6379> SET session:123 token-abc EX 10 OK 127.0.0.1:6379> TTL session:123 (integer) 9 127.0.0.1:6379> GET session:123 "token-abc" # 10 秒后再次访问 127.0.0.1:6379> TTL session:123 (integer) -2 127.0.0.1:6379> GET session:123 (nil)
注意:惰性删除 + 定期删除仍可能造成过期键暂时残留内存,内存敏感业务需合理设置过期时间和 maxmemory 淘汰策略。
6. Redis 的内存淘汰策略有哪些?
| 策略 | 含义 |
|---|---|
| noeviction | 不淘汰任何键,写入命令报错(默认) |
| volatile-lru | 设置了过期时间的键中淘汰最近最少使用 |
| volatile-lfu | 设置了过期时间的键中淘汰最近最不常用 |
| volatile-random | 设置了过期时间的键中随机淘汰 |
| volatile-ttl | 设置了过期时间的键中淘汰即将过期 |
| allkeys-lru | 所有键中淘汰最近最少使用 |
| allkeys-lfu | 所有键中淘汰最近最不常用 |
| allkeys-random | 所有键中随机淘汰 |
LRU vs LFU:
LRU关注「最近是否被使用」,很久没被访问即使曾高频也会被淘汰。
LFU关注「被使用频率」,淘汰访问次数低的键,防止偶发访问占用热度。
Redis 的 LRU 是近似实现,通过抽样计算兼顾性能和准确度。
选择经验:
纯缓存场景:
allkeys-lru或allkeys-lfu。既有缓存又有需持久保留的业务数据:
volatile-lru或volatile-lfu。
shell
# 查看当前淘汰策略 127.0.0.1:6379> CONFIG GET maxmemory-policy 1) "maxmemory-policy" 2) "noeviction" # 设置为 allkeys-lru 127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru OK # 设置最大内存为 1GB 127.0.0.1:6379> CONFIG SET maxmemory 1gb OK
第二部分:持久化篇
7. Redis 持久化机制 RDB 和 AOF 有什么区别?
RDB 持久化是快照式的,将某一时刻的内存数据以二进制形式保存到磁盘。
触发方式:
SAVE(阻塞主进程,生产禁用)、BGSAVE(fork 子进程,主进程继续)、save配置自动触发。优点:文件紧凑、恢复速度快,适合灾难恢复。
缺点:两次快照之间的数据可能丢失;fork 子进程在数据量大时会短暂阻塞。
AOF 持久化是追加式的,将每条写命令以文本形式记录到 AOF 文件。
三种同步策略:
always:每条命令都同步,安全性最高,性能最差。everysec:每秒同步一次,最多丢一秒数据(推荐)。no:由操作系统决定,性能最好,安全性最低。
优点:数据丢失少、文件可读。
缺点:文件体积大、恢复速度慢。
shell
# 查看持久化相关配置 127.0.0.1:6379> CONFIG GET save 1) "save" 2) "3600 1 300 100 60 10000" 127.0.0.1:6379> CONFIG GET appendonly 1) "appendonly" 2) "no" 127.0.0.1:6379> CONFIG GET appendfsync 1) "appendfsync" 2) "everysec"
上面的 save 配置表示:3600 秒内有 1 次修改、300 秒内有 100 次修改、60 秒内有 10000 次修改,满足任一条件就触发 BGSAVE。
核心区别对比:
| 对比维度 | RDB | AOF |
|---|---|---|
| 文件内容 | 某个时刻内存数据的二进制快照 | 记录所有写命令的文本格式 |
| 数据完整性 | 可能丢失最近一次快照之后的数据 | 最多丢失 1 秒(everysec 策略) |
| 恢复速度 | 快,直接加载即可恢复 | 慢,需要逐条回放命令 |
| 文件体积 | 紧凑、占用较小 | 持续增长、体积通常更大 |
| 可读性 | 二进制不可读 | 文本可读、必要时可人工修改 |
| 资源占用 | fork 子进程会有瞬时 CPU 和内存开销 | 追加写开销较低,但需要定期 AOF 重写 |
生产建议:RDB 与 AOF 同时开启,RDB 作为冷备和灾难恢复文件,AOF 作为数据恢复兜底。Redis 4.0 之后支持混合持久化,AOF 重写时前半部分使用 RDB 格式、后半部分追加增量命令,兼顾恢复速度和数据完整性。
第三部分:高可用与集群篇
8. Redis 主从复制的原理是什么?有什么优缺点?
主从复制是将一台 Redis 主节点数据复制到一台或多台从节点。核心价值:数据冗余备份、读写分离、为高可用方案提供基础。
复制流程:
建立连接:从节点执行
replicaof命令,与主节点建立连接并发送同步请求。全量同步:主节点执行
BGSAVE生成 RDB 快照发送给从节点,同时将生成快照期间的写命令写入复制缓冲区,随后一并发给从节点。增量同步:后续主节点通过
replication offset记录复制偏移量,将新增写命令持续发送给从节点。断线重连:从节点断线后重连,先尝试基于 offset 进行部分同步,不够用才退化为全量同步。
shell
# 在从节点执行,使其复制 127.0.0.1:6379 127.0.0.1:6380> REPLICAOF 127.0.0.1 6379 OK 127.0.0.1:6380> INFO replication # Replication role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up
优点:配置简单、读写分离效果明显。
缺点:复制是异步的,从节点数据可能延迟;主从切换时可能丢失少量数据;主节点自身仍是单点,宕机后需人工介入或依赖哨兵自动切换。
9. Redis 哨兵模式是什么?如何实现自动故障转移?
哨兵(Sentinel)是对主从复制架构的增强,本身不存储业务数据,以独立进程运行,负责监控主从节点状态并在主节点故障时自动完成主从切换。
四项职责:监控节点是否存活、发现故障后通知、自动完成故障转移、在客户端询问时返回当前主节点地址。
故障转移关键过程:
主观下线(SDOWN):单个哨兵发现主节点一段时间内无响应。
客观下线(ODOWN):多个哨兵达成一致,确认主节点已下线。
选举领导者:哨兵之间通过投票选出领导者负责本次故障转移。
选择新主:在健康从节点中按优先级、复制偏移量等因素选择最合适者提升为新主。
通知与收敛:其他从节点改为复制新主,客户端重新获取新主地址。
conf
# sentinel.conf 配置示例 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel auth-pass mymaster yourpassword sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000
配置中的 2 表示至少需要 2 个哨兵认为主节点下线,才触发客观下线和故障转移。生产环境推荐至少 3 个哨兵节点,尽量部署在不同物理机上。
10. Redis Cluster 集群的原理?数据如何分布?
当单机内存或写入能力成为瓶颈时,需要 Redis Cluster 进行水平扩展。集群由多个主节点组成,每个主节点负责整体数据的一部分,并通常为每个主节点配置从节点提供冗余。集群节点之间通过 Gossip 协议交换状态,不需要中心协调节点。
Redis Cluster 使用哈希槽实现数据分片,共 16384 个槽。每个 key 通过公式CRC16(key) % 16384计算所属槽位,每个主节点负责一部分槽。客户端可连接任意节点,如果请求的 key 不属于该节点,节点返回 MOVED 重定向。
哈希槽方案比一致性哈希更简单,也便于在线迁移。扩容时只需将部分槽从旧节点迁移到新节点:
shell
# 查看集群节点和槽位分布 127.0.0.1:6379> CLUSTER NODES # 查看某个 key 属于哪个槽 127.0.0.1:6379> CLUSTER KEYSLOT order:1001 (integer) 14577
优势:数据自动分片、支持在线扩容。
缺点:很多多 key 操作受槽位限制,跨槽的 mget、事务执行比较困难,需要使用 hash tag 将相关 key 落到同一个槽。
11. Redis 集群中如何定位某个 key 所在的节点?
定位 key 分三步:
对 key 计算 CRC16 校验和。
对 16384 取模得到槽号。
根据槽与节点的映射关系找到目标节点。
实际使用时客户端会缓存槽位映射表,直连目标节点减少重定向。
两种重定向:
MOVED:槽位稳定迁移时节点返回,告诉客户端正确的目标节点和槽,客户端更新本地映射。
ASK:槽位正在迁移过程中,表示该 key 暂时位于源节点或目标节点,此时只能临时访问指定节点,不应更新长期映射。
Hash tag可以让多个 key 落到同一槽:
shell
# 两个 key 都会使用 user:1001 计算槽位 127.0.0.1:6379> MSET {user:1001}:name tom {user:1001}:age 20 OK 127.0.0.1:6379> MGET {user:1001}:name {user:1001}:age 1) "tom" 2) "20"注意:hash tag 使用不当会导致数据倾斜,让大量 key 集中到少数槽和节点上。
12. Redis 主从切换过程中数据会丢失吗?如何减少丢失?
会。主从复制是异步的,主节点写入成功后立即给客户端返回 OK,此时数据可能还没同步到从节点。如果主节点突然宕机,这部分未同步的数据就可能丢失。此外,网络分区时哨兵提升的新主与旧主数据不一致,旧主恢复后被降级为从节点时可能覆盖数据,这就是典型的脑裂问题。
减少丢失的措施:
配置 min-replicas-to-write:要求至少 N 个从节点在线且延迟小于指定秒数时,主节点才接受写命令。
使用 WAIT 命令:关键写操作后主动等待指定数量的从节点确认同步,相当于把异步复制变成半同步。
部署偶数哨兵并合理设置 quorum:降低脑裂后选主错误的概率。
使用 AOF 持久化:即使主从数据不一致,也可以从 AOF 中恢复更多数据。
shell
# 要求至少 1 个从节点在线且延迟不超过 10 秒才允许写入 127.0.0.1:6379> CONFIG SET min-replicas-to-write 1 OK 127.0.0.1:6379> CONFIG SET min-replicas-max-lag 10 OK
明确一点:无论怎么配置,异步复制都无法做到严格不丢失。业务要求强一致时,需要在写入层增加额外确认机制或采用主从同步回执方案。
第四部分:缓存与一致性篇
13. 什么是缓存穿透、缓存击穿、缓存雪崩?如何解决?
三者都可能导致大量请求绕过缓存直接打到数据库,但成因不同,解决思路也不一样。
缓存穿透:查询一个缓存和数据库中都不存在的数据。
解决:对不存在的数据缓存空值并设置较短过期时间;使用布隆过滤器快速判断 key 是否可能存在。
缓存击穿:某个热点 key 在过期的一瞬间,大量并发请求同时穿透缓存打到数据库。
解决:热点数据设置逻辑过期;重建缓存时加互斥锁,只允许一个请求回源查询,其他请求等待或返回旧值。
缓存雪崩:大量 key 在同一时间段集中过期,或整个 Redis 实例宕机。
解决:过期时间加随机值、多级缓存、部署主从与哨兵保证高可用、限流降级。
给缓存空值防穿透:
shell
# 查询不存在的 key 时写入空值 127.0.0.1:6379> GET user:999999 (nil) 127.0.0.1:6379> SET user:999999 "" EX 60 OK
给过期时间加随机值防雪崩:基础过期时间 + 随机秒数,例如60 + random(0, 30),让缓存过期时间分散开。
14. 如何保证缓存与数据库的数据一致性?
缓存与数据库之间天然存在时间差,绝对一致几乎无法做到,工程上更多追求最终一致。
最常用的策略是 Cache Aside 模式:
读请求:先查缓存,命中直接返回,未命中则查数据库并回填缓存。
写请求:先更新数据库,再删除缓存,而不是更新缓存。
为什么推荐删缓存而不是更新缓存?很多场景下缓存的值需要经过计算或组装,直接更新不仅容易产生脏数据,还可能导致大量无用的缓存写入。删除缓存后,下一次读取会自动回源重建,逻辑更简单。
先更新数据库再删缓存的顺序是主流做法,但极端情况下仍可能出现短暂不一致。例如 A 更新数据库后、删除缓存前,B 读到旧缓存。更稳妥的方案是延迟双删:先删缓存,更新数据库,延迟一段时间后再删一次,覆盖并发场景。也可以借助 Canal 等工具监听数据库 binlog,异步驱动缓存失效,实现最终一致。
Cache Aside 读流程伪代码:
text
# value = cache.get(key) # if value is None: # value = db.query(key) # cache.set(key, value, ttl) # return value
如果业务能接受短暂不一致,删除缓存方案已经足够;若要求接近强一致,可考虑分布式事务、可重试的可靠消息或订阅 binlog 的异步补偿机制。
15. Redis 分布式锁如何实现?有哪些坑?
分布式锁的核心目标是在多个进程之间实现互斥访问。基于 Redis 实现时,最基础的做法是使用SET key value NX PX time原子命令:
NX:只有 key 不存在时才写入。PX:设置毫秒级过期时间。value:使用当前线程或请求的唯一标识,防止误删其他客户端的锁。
解锁必须小心,不能直接执行DEL,否则可能误删他人已获取的锁。正确做法是使用 Lua 脚本先比较 value 是否一致,再原子删除:
lua
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end常见的坑:
锁没有设置过期时间导致死锁。
业务执行时间超过锁过期时间导致锁提前释放。
使用非原子操作释放导致误删。
主从切换时锁信息未同步造成锁失效。
生产环境一般使用Redisson等成熟客户端,它提供了看门狗续期、可重入锁和公平锁等能力,避免手动实现出问题。
shell
# 加锁,token 为唯一标识,30 秒过期 127.0.0.1:6379> SET lock:order:1001 token-123 NX PX 30000 OK # 使用 Lua 脚本原子解锁 127.0.0.1:6379> EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:1001 token-123 (integer) 116. RedLock 算法靠谱吗?
RedLock 算法由 Redis 作者提出,核心思路是:向独立部署的多个 Redis 主节点依次尝试加锁,当在大多数节点加锁成功,并且加锁总耗时小于锁的有效时间时,认为获取锁成功;释放时向所有节点发送删除命令。
它的出发点是用多个独立节点降低单点故障风险。但该算法一直存在争议,马丁·克莱普曼等多位分布式系统专家曾指出,RedLock 依赖的是时钟而不是真正单调的版本号或租约,在 GC 停顿、网络延迟、进程暂停等情况下,仍可能出现多个客户端同时认为自己持有锁的情况,无法提供真正可靠的安全保证。
因此在实践中:
需要较高互斥保证时,更推荐使用基于强一致系统的方案,例如Zookeeper、etcd提供的分布式锁,或使用数据库唯一约束、乐观锁。
只是防止重复提交、限制告警频率等弱一致性场景,单 Redis 节点加锁 + 合理过期时间通常已经足够。
一句话总结:RedLock 在特定场景下可用,但它不是银弹,不能把它当作严格正确的分布式一致性原语。
第五部分:高频面试速查表
| 主题 | 核心要点 |
|---|---|
| Redis 定位 | 内存数据库,非单纯缓存,单线程事件循环 + IO 多路复用 |
| 为什么快 | 内存、高效数据结构、单线程、IO 多路复用、RESP 协议 |
| 数据类型 | String、Hash、List、Set、ZSet + Bitmap、HyperLogLog、GEO、Stream |
| 底层结构 | SDS、QuickList、Dict、IntSet、SkipList、ListPack |
| 过期删除 | 惰性删除 + 定期删除 + 内存淘汰兜底 |
| 内存淘汰 | noeviction、allkeys-lru/lfu/random、volatile-lru/lfu/random/ttl |
| RDB | 快照,恢复快,可能丢最近数据,fork 子进程 |
| AOF | 追加写命令,everysec 最多丢 1 秒,文件大恢复慢 |
| 主从复制 | 全量 + 增量,异步复制,读写在主从分离 |
| 哨兵 | 监控 + 自动故障转移 + 通知 + 配置提供者 |
| Cluster | 16384 个哈希槽,CRC16 取模,MOVED/ASK 重定向,hash tag |
| 缓存三大问题 | 穿透(布隆/空值)、击穿(互斥锁/逻辑过期)、雪崩(随机过期/多级缓存) |
| 缓存一致性 | Cache Aside,先更新数据库再删缓存,延迟双删,binlog 订阅 |
| 分布式锁 | SET NX PX + Lua 解锁,Redisson 看门狗,RedLock 有争议 |
总结
Redis 面试的核心主线可以浓缩为:
基础:定位、性能来源、数据类型、底层结构、过期与淘汰。
持久化:RDB 快照 + AOF 日志 + 混合持久化。
高可用:主从复制、哨兵、Cluster 哈希槽分片。
工程难题:缓存穿透/击穿/雪崩、缓存一致性、分布式锁、RedLock 争议。