Redis 面试题
本文整理 Java 后端开发高频面试题的 Redis 部分。每道题统一格式:题目 → 答案 → 对比表 / 代码示例 / 一句话记忆。
1. Redis 的作用
Redis 是基于内存的高性能 Key-Value 数据库,核心作用包括:
- 缓存:热点数据缓存,降低数据库压力,提升响应速度(最核心用途)。
- 分布式锁:基于 SETNX / Redisson 实现跨进程互斥。
- 限流:基于计数器、令牌桶、漏桶算法实现接口限流。
- 会话存储:分布式 Session 共享(Spring Session)。
- 排行榜:基于 ZSet 实现实时排序。
- 消息队列:基于 List、Stream 实现轻量级消息队列。
- 计数器:基于 INCR 实现点赞数、浏览量、库存扣减。
- 地理位置:基于 GEO 实现附近的人、门店定位。
💡 一句话记忆:缓存、分布式锁、限流、排行榜、消息队列、计数器——Redis 是后端"瑞士军刀"。
2. Redis 常见缓存问题
| 问题 | 描述 | 原因 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据,缓存和 DB 都没有 | 恶意攻击、查询不存在 key | ① 布隆过滤器拦截 ② 缓存空对象(设短 TTL) |
| 缓存击穿 | 热点 key 过期瞬间,大量请求打到 DB | 热点 key 过期 | ① 热点 key 永不过期 ② 加互斥锁重建缓存 |
| 缓存雪崩 | 大量 key 同时过期,或 Redis 宕机 | TTL 集中、Redis 故障 | ① TTL 加随机值打散 ② Redis 高可用集群 ③ 限流降级 ④ 数据预热 |
三者对比要点:
- 穿透是「查不到」(数据本不存在)。
- 击穿是「一个热点 key 过期」。
- 雪崩是「大量 key 同时过期或 Redis 挂了」。
💡 一句话记忆:穿透查不到(布隆/空值)、击穿一个热 key 过期(互斥锁/永不过期)、雪崩大批过期(随机 TTL + 高可用)。
3. 布隆过滤器
布隆过滤器(Bloom Filter)是一种空间效率很高的概率型数据结构,用于判断一个元素是否可能在集合中。
原理:
- 由一个很长的二进制位数组(bit array)和多个无偏哈希函数组成。
- 写入元素时,用 k 个哈希函数对元素计算,得到 k 个位置,将这些位置置为 1。
- 查询时,同样用 k 个哈希函数计算,若所有位置都为 1 → 可能存在;若有任一位置为 0 → 一定不存在。
写入 "hello":
hash1("hello") = 3 → bit[3]=1
hash2("hello") = 7 → bit[7]=1
hash3("hello") = 11 → bit[11]=1
查询 "world":
hash1("world") = 3 → bit[3]=1 ✓
hash2("world") = 9 → bit[9]=0 ✗ → 一定不存在核心特性:
| 特性 | 说明 |
|---|---|
| 空间效率 | 极高,1 亿数据约需 100MB |
| 查询时间 | O(k),k 为哈希函数个数 |
| 误判率 | 存在误判(假阳性),但不会漏判 |
| 删除支持 | 不支持删除(多个元素可能共享同一个 bit) |
关键参数:
- 位数组大小 m:越大误判率越低,空间越大。
- 哈希函数个数 k:过多则 bit 被快速填满,过少则冲突多。
- 预期元素数 n 与 误判率 p 决定 m 和 k:
m = -(n·ln p) / (ln 2)²,k = (m/n)·ln 2。
为什么不能删除?
因为一个 bit 可能被多个元素映射置 1,直接置 0 会影响其他元素的判断。如需删除,可使用布谷鸟过滤器或计数布隆过滤器(每个位置用计数器代替 bit)。
Redis 中使用: Redis 4.0+ 通过 RedisBloom 模块支持布隆过滤器,常用命令 BF.ADD、BF.EXISTS。
💡 一句话记忆:布隆过滤器 = 位数组 + 多哈希;说"没有"一定没有,说"有"可能误判;不支持删除。
4. Redis 实现排行榜功能
利用 Redis 的 ZSet(Sorted Set) 实现排行榜,ZSet 自动按 score 排序且去重。
实现要点:
- Key 设计:用业务维度 + 时间维度命名,如
rank:week:202629(2026 年第 29 周)。 - 同分排序问题:分数相同时 ZSet 按 member 字典序排序,可通过组合 score 解决(如
score = 实际分数 × 1e13 + (最大时间戳 - 当前时间戳),让先达分的排前)。 - 多字段排序:将多个字段组合成一个 score 值(如「胜场 × 1000 + 净胜分」)。
- 数据备份:定期将排行榜快照持久化到数据库,防止 Redis 数据丢失。
常用命令:
# 添加/更新成员分数
ZADD rank:week:202629 1500 "user:001"
# 获取 Top 10(分数从高到低)
ZREVRANGE rank:week:202629 0 9 WITHSCORES
# 获取某成员排名
ZREVRANK rank:week:202629 "user:001"
# 增加分数
ZINCRBY rank:week:202629 100 "user:001"💡 一句话记忆:排行榜用 ZSet,ZADD 加分、ZREVRANGE 取 Top N、ZREVRANK 取排名;同分用组合 score。
5. Redis 相比 Memcached 有哪些优势?
| 对比项 | Redis | Memcached |
|---|---|---|
| 数据类型 | 丰富(String/List/Hash/Set/ZSet 等) | 仅 String |
| 持久化 | 支持 RDB / AOF | 不支持 |
| 集群 | 原生支持 Cluster | 需客户端一致性哈希分片 |
| 单线程模型 | 是(6.0 前单线程,6.0 后 IO 多线程) | 是 |
| 线程安全 | 单线程命令处理,天然安全 | 多线程,需 CAS |
| 事务 | 支持(MULTI/EXEC) | 不支持 |
| 发布订阅 | 支持 | 不支持 |
| 内存利用率 | 有内部编码优化(如 ziplist) | 简单 slab 分配 |
核心优势:
- 数据类型丰富:Memcached 所有值均为简单字符串,Redis 支持更为丰富的数据类型,可直接实现排行榜、计数器、消息队列等功能。
- 速度更快:Redis 单线程无锁,纯内存操作,性能优秀。
- 可持久化:Redis 支持将内存数据持久化到磁盘,重启不丢数据。
💡 一句话记忆:Redis 比 Memcached 多了数据类型、持久化、集群、事务——更全能。
6. 分布式锁
分布式锁用于在分布式系统中实现跨进程的互斥访问。
(1)基础实现:SETNX
Redis 实现分布式锁的基础指令是 SETNX(Set if Not eXists):
SETNX key value → key 不存在则设置成功(获得锁),存在则失败SETNX 的两个问题:
- 死锁问题:持锁进程崩溃,未释放锁 → 其他进程永远拿不到锁。
- 误删问题:A 持锁超时后 B 获得锁,A 恢复后误删了 B 的锁。
解决方案(原子加锁 + 唯一标识 + Lua 解锁):
# 加锁:设置唯一 value + 过期时间(原子操作)
SET lock_key {unique_id} NX PX 30000-- 解锁:Lua 脚本保证「判断 + 删除」原子性
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end(2)Redisson(生产推荐)
Redisson 是 Redis 的 Java 客户端,内置分布式锁实现,解决上述问题:
- 自动续期(看门狗 Watchdog):锁默认 30 秒过期,后台线程每 10 秒续期到 30 秒,防止业务未完成锁就过期。
- 可重入:基于 Hash 结构记录重入次数。
- 公平锁、读写锁、信号量支持。
RLock lock = redisson.getLock("orderLock");
try {
lock.lock(); // 默认开启看门狗续期
// 业务逻辑
} finally {
lock.unlock();
}看门狗续期机制:
- 加锁时不指定过期时间 → 启动看门狗,默认每 10 秒检查并续期到 30 秒。
- 加锁时指定过期时间 → 不启动看门狗(锁到期自动释放)。
(3)RedLock(红锁,多节点)
在 Redis 主从架构中,主节点加锁后未同步到从节点就宕机,会导致锁丢失。RedLock 由 Redis 作者提出:
- 在 N(通常 5)个独立 Redis 节点上同时加锁。
- 超过半数(N/2 + 1)节点加锁成功,且总耗时小于锁过期时间 → 加锁成功。
- 否则向所有节点释放锁。
| 方案 | 优点 | 缺点 |
|---|---|---|
| SETNX + Lua | 实现简单 | 死锁、误删风险,单点故障 |
| Redisson | 自动续期、可重入 | 主从切换仍可能丢锁 |
| RedLock | 高可用、抗节点故障 | 实现复杂、有争议(Martin Kleppmann 质疑) |
💡 一句话记忆:SETNX 加唯一值 + 过期时间 + Lua 解锁;生产用 Redisson(看门狗续期);强一致用 RedLock 或 ZooKeeper。
7. 如何保证 Redis 中的数据都是热点数据?
Redis 内存数据集大小上升到一定限制时,会通过内存淘汰策略删除非热点数据。Redis 4.0 之前有 6 种,4.0 之后新增 2 种(LFU),共 8 种:
| 策略 | 说明 | 适用场景 |
|---|---|---|
noeviction | 不淘汰,内存满时写入报错(默认) | 不允许丢数据 |
allkeys-lru | 所有 key 中淘汰最近最少使用 | 通用缓存 |
allkeys-lfu | 所有 key 中淘汰最不常用(4.0+) | 访问频率差异大 |
allkeys-random | 所有 key 中随机淘汰 | 无明显访问模式 |
volatile-lru | 设置了过期时间的 key 中淘汰 LRU | 混合持久化 + 缓存 |
volatile-lfu | 设置了过期时间的 key 中淘汰 LFU(4.0+) | 混合场景 |
volatile-random | 设置了过期时间的 key 中随机淘汰 | - |
volatile-ttl | 设置了过期时间的 key 中淘汰即将过期的 | 短期数据 |
LRU vs LFU:
- LRU(Least Recently Used):最近最少使用,基于"时间"维度,淘汰最久没访问的。
- LFU(Least Frequently Used):最不经常使用,基于"频率"维度,淘汰访问次数最少的。Redis 4.0+ 引入,更适合区分"偶尔被访问一次"和"高频访问"的 key。
💡 一句话记忆:8 种策略 = noeviction + allkeys 3 种(lru/lfu/random)+ volatile 4 种(lru/lfu/random/ttl);纯缓存用 allkeys-lru,4.0+ 优先 allkeys-lfu。
8. Redis 的并发竞争问题如何解决?
Redis 为单进程单线程模式(命令执行),采用队列模式将并发访问变为串行访问,单实例下本身不存在并发竞争问题。但在以下场景会出现竞争:
场景 1:多客户端同时修改同一 key(非原子操作)
# 客户端 A: get → 100 → set 101
# 客户端 B: get → 100 → set 101
# 期望 102,实际 101(丢失更新)解决方案:
- 使用原子命令:用
INCR、DECR、INCRBY等原子操作替代 get + set。 - 使用 Lua 脚本:将多步操作封装为 Lua 脚本,Redis 保证原子执行。
- 使用分布式锁:用 Redisson 加锁,串行化关键操作。
- 使用事务(MULTI/EXEC)+ WATCH:乐观锁机制,WATCH 监视 key,若被修改则事务失败重试。
# WATCH + MULTI 实现乐观锁
WATCH balance
val = GET balance
MULTI
SET balance val + 100
EXEC
# 若 balance 在 WATCH 后被改,EXEC 返回 nil,重试场景 2:主从切换导致数据不一致
多个客户端在主从切换期间分别写入新旧主节点,导致数据不一致 → 使用分布式锁或 RedLock。
💡 一句话记忆:单线程无并发,但多步非原子操作会竞争 → 用原子命令 / Lua / 分布式锁 / WATCH 事务解决。
9. Redis 是单线程还是多线程?
Redis 的"单线程"和"多线程"要分版本和模块来看:
| 版本 | 网络请求 | 命令执行 | 持久化/集群同步 |
|---|---|---|---|
| 6.0 之前 | 单线程 | 单线程 | 多线程(BIO) |
| 6.0+ | 多线程(IO 读写) | 单线程 | 多线程(BIO) |
详细说明:
- Redis 6.0 之前:单线程指的是其网络 I/O 和键值对读写都由一个线程完成。
- Redis 6.0 引入多线程:指的是网络请求过程(读取 socket、解析协议、写回响应)采用了多线程,而键值对读写命令仍然是单线程处理,所以 Redis 依然是并发安全的。
- 持久化(RDB/AOF)、集群数据同步、延迟删除等操作由额外的后台线程执行。
为什么单线程还这么快?
- 完全基于内存,读写极快。
- 单线程无锁、无上下文切换、无竞争。
- 使用 IO 多路复用(epoll)处理大量连接。
- 数据结构高效(如 ziplist、skiplist)。
💡 一句话记忆:6.0 前全单线程,6.0 后网络 IO 多线程、命令执行仍单线程;快靠内存 + 多路复用 + 无锁。
10. Redis 过期删除策略和内存淘汰策略有什么区别?
两者都是 Redis 中用于管理内存的策略,但触发条件和目的不同。
| 对比项 | 过期删除策略 | 内存淘汰策略 |
|---|---|---|
| 触发条件 | key 设置了过期时间且已过期 | 内存使用达到 maxmemory 上限 |
| 目的 | 删除过期的键值对,避免占用内存 | 内存不足时释放空间,保证写入可用 |
| 作用对象 | 仅设置了 TTL 的 key | 所有 key 或带 TTL 的 key(视策略) |
| 时机 | key 过期时 | 内存满时 |
Redis 过期删除策略(两种组合):
- 惰性删除:访问 key 时才检查是否过期,过期则删除。优点是 CPU 友好,缺点是过期 key 长期不访问会占用内存。
- 定期删除:每隔一段时间随机抽取一批设置了 TTL 的 key 检查,删除过期的。Redis 默认每秒 10 次,每次抽取 20 个 key。
Redis 采用 惰性删除 + 定期删除 组合策略,互补两者的不足。
Redis 内存淘汰策略: 见前文「如何保证热点数据」一题的 8 种策略。
💡 一句话记忆:过期删除管"到期 key"(惰性 + 定期),内存淘汰管"内存满"(8 种策略)。
11. Redis 应用场景
Redis 提供丰富的数据结构,每种都有典型应用场景:
| 数据结构 | 典型应用场景 |
|---|---|
| String | 缓存对象、常规计数(PV/UV)、分布式锁、限速器、共享 Session |
| List | 栈(LPUSH + LPOP)、队列(LPUSH + RPOP)、阻塞队列(BLPOP)、消息队列、最新 N 条记录 |
| Hash | 存储对象(用户信息、商品信息)、购物车(用户 → 商品 → 数量) |
| Set | 黑白名单、抽奖(SRANDMEMBER)、标签、共同好友(SINTER)、去重 |
| ZSet | 排行榜、延迟队列(score 为执行时间)、带权重的消息队列 |
| BitMap | 用户签到、活跃用户统计(BITCOUNT)、布隆过滤器基础 |
| HyperLogLog | UV 统计(基数统计,误差 0.81%,固定 12KB) |
| Geo | 附近的人、附近的店、距离计算(GEOADD / GEORADIUS) |
| Stream(5.0+) | 消息队列(支持消费组、ACK,替代 List 实现 MQ) |
💡 一句话记忆:String 计数锁缓存、List 队列、Hash 对象、Set 去重交集、ZSet 排行榜、BitMap 签到、Geo 附近、Stream 消息队列。