Redis 是面试中高频出现的中间件,涉及基础数据结构、持久化、高可用、缓存问题等多个维度。以下是常见面试题及核心要点:
一、基础概念与特性
-
什么是 Redis?它有哪些核心特性?
- 定义:Redis 是开源的高性能键值对内存数据库,支持多种数据结构,可用于缓存、分布式锁、消息队列等场景。
- 核心特性:
- 基于内存操作,速度快(毫秒级响应);
- 支持丰富的数据结构(String、Hash、List、Set、Sorted Set 等);
- 支持持久化(RDB、AOF),避免内存数据丢失;
- 支持主从复制、哨兵、集群,保证高可用;
- 单线程模型(核心逻辑单线程,避免线程切换开销);
- 支持 Lua 脚本、事务、发布订阅等。
-
Redis 为什么这么快?
- 内存存储:数据存在内存中,避免磁盘 IO 开销(磁盘 IO 是毫秒级,内存是纳秒级)。
- 单线程模型:核心读写逻辑用单线程,避免多线程切换和锁竞争(Redis 6.0 后 IO 线程可并行处理网络请求,但核心逻辑仍单线程)。
- 高效数据结构:针对不同场景优化数据结构(如 String 用 SDS 动态字符串,Hash 用哈希表 + 压缩列表)。
- IO 多路复用:用 epoll/kqueue 等 IO 多路复用模型,单线程处理多个客户端连接,减少阻塞。
-
Redis 支持哪些数据结构?各自的应用场景是什么?
数据结构 底层实现(简化) 核心特性 应用场景 String(字符串) 简单动态字符串(SDS) 二进制安全,支持拼接、自增 / 减 缓存(如用户信息)、计数器、分布式 ID Hash(哈希) 哈希表 + 压缩列表(小数据) 键值对集合,适合存储对象 存储用户信息(name/age/addr) List(列表) 双向链表 + 压缩列表(小数据) 有序,可重复,支持两端操作 消息队列(lpush + rpop)、最新列表 Set(集合) 哈希表 + 整数集合(小整数) 无序,不可重复,支持交集 / 并集 好友关系(共同好友)、去重 Sorted Set(有序集合) 跳表 + 哈希表 有序(按 score 排序),不可重复 排行榜(如销量排名)、延时队列 -
Redis 的单线程模型是怎样的?为什么单线程还能处理高并发?
- 单线程模型:Redis 核心的 "网络 IO" 和 "数据操作" 逻辑由一个主线程处理,避免多线程的上下文切换和锁开销。
- 高并发原因:
- 内存操作速度极快,单线程足以处理大部分请求;
- 用 IO 多路复用(如 epoll)同时监听多个客户端连接,主线程通过 "事件循环" 处理就绪的 IO 事件(读 / 写),无需阻塞等待。
- 注意:Redis 6.0 引入 "多 IO 线程",仅负责网络数据的读写(解析命令、返回结果),核心的数据处理仍单线程,进一步提升并发能力。
二、持久化机制
-
**Redis 的持久化机制有哪些?(RDB vs AOF)**持久化用于将内存数据写入磁盘,避免宕机丢失,两种核心方式:
维度 RDB(快照) AOF( Append Only File) 原理 定时生成内存全量数据的二进制快照( dump.rdb
)记录所有写命令到日志文件( appendonly.aof
),恢复时重放命令触发方式 手动( save
/bgsave
)、自动(配置save <秒> <次数>
)实时追加(依赖 appendfsync
策略)优点 文件小,恢复速度快;适合备份 数据丢失少(最多丢失 1 秒);命令可读 缺点 快照间隔内数据可能丢失; bgsave
fork 子进程耗资源文件大,恢复慢;写命令追加可能影响性能 适用场景 数据允许短期丢失,需快速恢复 数据安全性要求高(如金融场景) -
RDB 的
save
和bgsave
有什么区别?save
:主线程执行,会阻塞 Redis 服务(期间无法处理客户端请求),适合停机备份。bgsave
:主线程fork
一个子进程负责生成 RDB 文件,主线程继续处理请求(非阻塞),是默认推荐方式。- 注意:
fork
子进程时会复制内存页表(写时复制),若内存大,fork
可能短暂阻塞主线程。
-
AOF 的重写机制是什么?为什么需要重写?
- 原因:AOF 文件会因重复命令(如多次
set key value
)越来越大,导致恢复慢、占用磁盘空间。 - 重写机制:通过
bgrewriteaof
命令(或自动触发),生成一个 "精简版" AOF 文件 ------ 直接记录数据的最终状态(如将set a 1; set a 2
合并为set a 2
)。 - 流程:主线程
fork
子进程,子进程遍历内存数据生成新 AOF 命令;重写期间新命令写入 "重写缓冲区",完成后追加到新文件,替换旧文件。
- 原因:AOF 文件会因重复命令(如多次
-
Redis 4.0 后的混合持久化是什么?
- 混合持久化:RDB 作为 AOF 文件的开头(存储全量数据快照),后续追加 AOF 增量命令。
- 优势:结合 RDB 恢复快和 AOF 数据全的优点 ------ 恢复时先加载 RDB 基础数据,再执行 AOF 增量命令,兼顾速度和安全性。
三、高可用与分布式
-
Redis 主从复制的原理是什么?
- 主从复制:通过复制将主节点(Master)的数据同步到从节点(Slave),实现读写分离(Master 写,Slave 读)和数据备份。
- 核心流程:
- 从节点连接主节点,发送
SYNC
命令(Redis 2.8 前)或PSYNC
命令(支持部分同步)。 - 主节点
bgsave
生成 RDB 文件,发送给从节点;从节点清空旧数据,加载 RDB。 - 主节点将 RDB 生成期间的写命令记录到 "复制缓冲区",发送给从节点;从节点执行这些命令,与主节点数据一致。
- 后续主节点的写命令会实时同步到从节点(增量复制)。
- 从节点连接主节点,发送
-
哨兵(Sentinel)的作用是什么?工作流程是怎样的?
- 作用:监控主从节点状态,当主节点宕机时自动将从节点升级为新主节点(故障转移),保证 Redis 高可用。
- 工作流程:
- 监控 :哨兵定期向主从节点发送
PING
命令,判断节点是否存活(主观下线)。 - 判断:若主节点主观下线,哨兵集群投票(超过半数同意),标记为主节点 "客观下线"。
- 故障转移 :
- 从哨兵中选举一个 "领导者" 负责处理故障转移。
- 从所有从节点中选一个 "最优从节点"(如数据最新、优先级高)作为新主节点。
- 让其他从节点复制新主节点,原主节点恢复后作为从节点。
- 监控 :哨兵定期向主从节点发送
-
Redis 集群(Cluster)的架构是什么?如何实现分片?
- 架构:Redis 集群是分布式存储方案,由多个主从节点组成(每个主节点可带从节点),解决单节点容量和性能瓶颈。
- 分片机制:
- 集群将数据分为 16384 个 "槽位(slot)",每个主节点负责一部分槽位(如 3 主节点,各负责~5461 个槽)。
- 数据存储时,通过
CRC16(key) % 16384
计算 key 所属槽位,再路由到负责该槽位的主节点。
- 高可用:主节点宕机后,其从节点会升级为新主节点,接管槽位。
-
Redis 集群如何处理槽位迁移?
- 槽位迁移:当集群扩容 / 缩容时,需要将槽位从旧主节点迁移到新主节点,保证数据均衡。
- 流程(简化):
- 源节点将槽位中的 key 逐个迁移到目标节点(先迁移,再更新槽位映射)。
- 迁移期间,客户端访问该槽位的 key 时,源节点会返回
ASK
重定向,指引客户端到目标节点。 - 所有 key 迁移完成后,更新集群槽位映射表(所有节点同步新映射)。
四、缓存问题与解决方案
-
什么是缓存穿透?如何解决?
- 缓存穿透:查询不存在的数据(如 id=-1 的用户),缓存和数据库都无结果,导致请求每次都穿透到数据库,压垮 DB。
- 解决方案:
- 空值缓存 :缓存不存在的 key(如
id=-1 → null
),设置短期过期时间(避免缓存大量空值)。 - 布隆过滤器:提前将所有存在的 key 存入布隆过滤器,请求先过过滤器,不存在则直接返回(误判率低,适合海量数据)。
- 空值缓存 :缓存不存在的 key(如
-
什么是缓存击穿?如何解决?
- 缓存击穿:热点 key 突然过期,此时大量请求同时穿透到数据库,导致 DB 压力骤增。
- 解决方案:
- 热点 key 永不过期:在缓存层不设置过期时间,由业务层定期更新(适合非实时数据)。
- 互斥锁 :第一个请求获取锁后查询 DB 并更新缓存,其他请求等待锁释放后从缓存获取(用 Redis 的
setnx
实现锁)。 - 预热与过期时间错开:提前加载热点数据,设置随机过期时间(避免大量 key 同时过期)。
-
什么是缓存雪崩?如何解决?
- 缓存雪崩:大量 key 同时过期,或缓存集群宕机,导致所有请求穿透到数据库,DB 被压垮。
- 解决方案:
- 过期时间错开 :给 key 过期时间加随机值(如
expire + 1~5 分钟
),避免同时过期。 - 缓存集群高可用:部署主从 + 哨兵或集群,避免单点故障(如一台缓存机宕机,其他节点仍可用)。
- 限流降级:用 Sentinel/Hystrix 对 DB 限流,超出阈值则返回降级数据(如 "系统繁忙")。
- 多级缓存:本地缓存(如 Caffeine)+ 分布式缓存(Redis),减少分布式缓存压力。
- 过期时间错开 :给 key 过期时间加随机值(如
-
缓存与数据库一致性如何保证?
- 核心原则:最终一致性 (强一致性难实现,需权衡性能)。常见方案:
-
更新策略 :先更数据库,再删缓存(而非更新缓存)------ 避免 "脏写"(如 A 更新 DB 后更新缓存,B 同时更新 DB 并覆盖缓存,导致 A 的更新丢失)。
java
运行
// 伪代码 updateDB(); // 先更新数据库 deleteCache(); // 再删除缓存(下次查询会从 DB 加载最新数据到缓存)
-
延迟双删:解决 "删除缓存失败" 的问题 ------ 更新 DB 后删缓存,隔一段时间(如 500ms)再删一次(应对第一次删除失败)。
-
读写分离场景:主从复制延迟可能导致 "查从库时缓存未更新",可在删除缓存后,短暂禁止从库读(或设置缓存过期时间小于主从延迟)。
-
- 核心原则:最终一致性 (强一致性难实现,需权衡性能)。常见方案:
五、过期策略与内存管理
-
Redis 的过期键删除策略是什么?
- Redis 用三种策略结合处理过期键:
- 惰性删除:访问 key 时才检查是否过期,过期则删除(节省 CPU,可能浪费内存)。
- 定期删除:每隔一段时间(默认 100ms),随机抽查部分过期 key 并删除(平衡 CPU 和内存)。
- 内存淘汰机制 :当内存达到
maxmemory
阈值,触发淘汰策略(如删除部分 key 释放内存)。
-
Redis 的内存淘汰机制有哪些?
- 当内存满时,根据配置的
maxmemory-policy
淘汰 key:volatile-lru
:从设置了过期时间的 key 中,淘汰最近最少使用的。allkeys-lru
:从所有 key 中,淘汰最近最少使用的(最常用)。volatile-lfu
:从设置过期时间的 key 中,淘汰最不经常使用的。allkeys-lfu
:从所有 key 中,淘汰最不经常使用的。volatile-random
:从设置过期时间的 key 中,随机淘汰。allkeys-random
:从所有 key 中,随机淘汰。volatile-ttl
:从设置过期时间的 key 中,淘汰剩余 TTL 最小的。noeviction
:不淘汰,返回错误(默认,不推荐)。
- 当内存满时,根据配置的
六、高级特性与实践
-
Redis 如何实现分布式锁?
-
分布式锁用于解决分布式系统中资源竞争问题(如秒杀库存),Redis 实现核心是
set
命令:bash
# 加锁:key=lock:xxx,value=唯一标识(如UUID),NX(不存在才设置),PX(过期时间,防死锁) SET lock:product:1001 uuid-xxx NX PX 30000
-
解锁:需用 Lua 脚本保证原子性(先判断 value 是否为自己的标识,再删除): lua
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
-
问题:单点 Redis 可能宕机,可用 RedLock 算法(多个 Redis 实例加锁,超过半数成功才认为锁有效)。
-
-
Redis 的大 key 有什么危害?如何处理?
- 危害:
- 占用大量内存,导致内存分布不均;
- 序列化 / 反序列化耗时,影响性能;
- 删除大 key 可能阻塞主线程(如
del
一个百万元素的 Hash)。
- 处理:
- 拆分大 key :如将大 Hash 拆分为多个小 Hash(
user:1000 → user:1000:info
、user:1000:orders
)。 - 渐进式删除 :用
hscan
/sscan
分批删除大 key 的元素(如每次删 100 个),避免阻塞。 - 监控预警 :通过
redis-cli --bigkeys
定期检测大 key,设置阈值(如 >10MB 告警)。
- 拆分大 key :如将大 Hash 拆分为多个小 Hash(
- 危害:
-
Redis 事务的特性是什么?有什么局限性?
- 特性:Redis 事务通过
multi
(开始)、exec
(执行)、discard
(取消)实现,支持:- 批量执行命令(
exec
时一次性执行); - 原子性(要么全执行,要么全不执行,中间命令失败不回滚)。
- 批量执行命令(
- 局限性:
- 没有隔离级别(事务执行期间,其他客户端的命令可能插入);
- 命令错误分为 "语法错"(
exec
前报错,事务取消)和 "运行错"(如对 String 用hset
,exec
会继续执行,不回滚); - 不支持回滚(设计上为了性能,避免回滚日志开销)。
- 特性:Redis 事务通过