redis相关面试题
1.缓存穿透(Cache Penetration)
-
问题描述: 查询一个数据库中根本不存在的数据。由于缓存中不会有该数据(未命中),导致每次请求都直接访问数据库,给数据库造成巨大压力,甚至压垮数据库。
-
原因:
-
恶意攻击:攻击者故意构造大量不存在的ID进行查询。
-
业务逻辑错误:程序BUG导致产生大量无效请求。
-
-
解决方案:
-
缓存空对象(Null Caching / Bloom Filter Pre-caching): 即使数据库查询为空,也将这个空结果(比如
null
)进行短暂的缓存(设置一个较短的过期时间,如 1-5 分钟)。这样后续相同的无效请求在缓存过期前会命中空对象,保护数据库。-
优点: 实现简单。
-
缺点: 1) 消耗额外的内存存储大量空值。2) 存在短期的数据不一致(如果这个key在数据库里被创建了,在空对象过期前可能查不到)
-
-
布隆过滤器(Bloom Filter): 在访问缓存之前,先经过一个布隆过滤器。
-
原理: 布隆过滤器是一个概率型数据结构,可以高效地判断一个元素一定不存在或可能存在于某个集合中。
-
工作流程:
-
将所有可能存在的有效键(或键的哈希值)预先加载到布隆过滤器中。
-
请求到来时,先用布隆过滤器判断请求的Key:
-
如果布隆过滤器说不存在 -> 则直接返回空或错误,无需查询缓存和数据库(拦截无效请求)。
-
如果布隆过滤器说可能存在 -> 则继续走正常的缓存查询逻辑(查询缓存,未命中则查数据库)。
-
-
-
优点: 内存占用远小于缓存空对象,能有效拦截绝大部分无效请求。
-
缺点: 1) 存在一定的误判率(False Positive - 判断为可能存在,但实际上不存在,这种情况仍然会走到数据库查询,但概率可控)。2) 删除数据困难(需要维护布隆过滤器的更新,通常结合空对象缓存使用)。3) 需要预加载有效键集合。
-
-
接口层校验: 对请求参数进行基础校验(如ID范围、格式校验),拦截明显无效的请求。
-
2.缓存击穿(Cache Breakdown)
-
问题描述: 某个热点数据Key在缓存中恰好过期的瞬间,有大量的并发请求同时涌来。这些请求发现缓存失效,都会去后端数据库加载数据,导致数据库瞬间压力剧增甚至崩溃。
-
原因: 热点Key + 缓存同时失效 + 高并发。
-
与穿透的区别: 击穿针对的是存在但暂时失效的热点数据;穿透是针对根本不存在的数据。
-
解决方案:
-
设置热点数据永不过期: 对于一些极热点的数据,可以设置永不过期(或物理上不设置过期时间)。通过逻辑过期(在Value中存储一个过期时间字段)或后台异步更新策略来保证数据的更新。
-
工作流程:
-
缓存永不过期,Value中包含逻辑过期时间
expireTime
。 -
查询时,先返回缓存数据。
-
程序判断
expireTime
是否已过:-
未过:直接返回数据。
-
已过:触发异步线程去更新缓存,当前请求仍返回旧数据(或稍作等待)。
-
-
-
优点: 避免缓存同时失效。
-
缺点: 需要额外字段和异步更新逻辑,可能返回短暂旧数据。
-
-
互斥锁(Mutex Lock):
-
工作流程:
-
当缓存失效时,不是所有线程都去查数据库。
-
第一个发现失效的线程尝试获取一个分布式锁(例如使用Redis的
SET key value NX PX milliseconds
命令)。 -
获取锁成功的线程负责查询数据库并更新缓存。
-
其他线程等待(轮询或订阅通知)或者短暂休眠后重试读取缓存。
-
-
优点: 保证只有一个线程去查数据库,保护数据库。
-
缺点: 1) 未获取锁的线程需要等待,增加延迟。2) 分布式锁的实现复杂度。3) 锁失效时间设置不当可能导致死锁或重复查询。
-
-
缓存预热(Cache Warm-up): 在系统启动或低峰期,提前将热点数据加载到缓存中,尽量避免在高峰期出现缓存失效。
-
3.缓存雪崩(Cache Avalanche)
-
问题描述: 在某一时刻,大量的缓存Key同时失效(或Redis服务不可用),导致所有原本应该命中缓存的请求都涌向后端数据库,造成数据库压力骤增甚至崩溃。
-
原因:
-
大量Key设置了相同的过期时间(例如系统初始化时批量加载数据,设置了相同的TTL)。
-
Redis服务宕机或集群故障。
-
-
与击穿的区别: 雪崩是大量Key同时失效;击穿是单个热点Key失效。
-
解决方案:
-
设置不同的过期时间: 这是最常用且有效的方法。给缓存数据的过期时间加上一个随机值(例如基础过期时间 + 随机几分钟)。确保数据不会在同一时间大面积失效,而是分散失效。
-
构建高可用缓存集群:
-
Redis Sentinel(哨兵): 实现主从切换和故障转移,提高可用性。
-
Redis Cluster(集群): 提供数据分片(Sharding)和高可用,即使部分节点宕机,集群整体仍能提供服务(部分数据可能暂时不可用)。
-
-
服务熔断与降级:
-
熔断(Circuit Breaker): 当检测到数据库访问失败率高或响应过慢时,暂时“熔断”对数据库的访问,直接返回预设的默认值(或错误信息),给数据库恢复的时间。
-
降级(Degradation): 在系统压力过大时,暂时关闭一些非核心功能或返回简化数据,优先保证核心功能的可用性(可能核心功能也会用到缓存,但压力会小些)。
-
-
多级缓存: 在应用层(如本地缓存Guava Cache/Caffeine)和分布式缓存(Redis)之间再加一层缓存。即使Redis挂了,本地缓存还能抵挡一部分流量,为恢复争取时间。需要注意本地缓存的一致性管理。
-
缓存永不过期 + 后台更新: 类似击穿的解决方案,对关键数据使用。
-
4.内存淘汰策略决策
策略 | 作用范围 | 淘汰依据 | 适用场景 |
---|---|---|---|
allkeys-lru | 所有Key | 最近最少使用 | 通用缓存(保留热点数据) |
volatile-lru | 带过期时间的Key | 最近最少使用 | 区分永久/临时数据 |
allkeys-lfu | 所有Key | 访问频率最低 | 防扫描访问导致缓存污染 |
volatile-ttl | 带过期时间的Key | TTL最短 | 优先清理即将过期数据 |
noeviction | - | 拒绝写入 | 数据绝对不可丢失场景 |
5. 缓存的数据结构
常用结构及典型场景:
-
String:
-
场景:简单KV缓存(用户信息序列化JSON存储)、计数器(
INCR/DECR
)、分布式锁(SETNX
)、位操作(签到统计)。
-
-
Hash:
-
场景:存储对象(如用户信息,每个field对应一个属性)。适合需要部分修改(
HSET
单个field)或部分读取(HGET
单个field)的场景。比String序列化整个对象更节省网络流量和内存(Redis内部优化)。
-
-
List:
-
场景:消息队列(
LPUSH
/RPOP
,BRPOP
阻塞版本)、最新消息/动态列表(LPUSH
+LTRIM
保持固定长度)、栈(LPUSH
/LPOP
)。
-
-
Set:
-
场景:无序集合(标签Tag、用户关注列表)、去重(UV统计初步去重)、集合运算(交集
SINTER
/并集SUNION
/差集SDIFF
- 共同好友、推荐)。
-
-
Sorted Set (ZSet):
-
场景:排行榜(
ZADD
+ZREVRANGE
)、带权重的队列(延迟队列 - 时间戳作为score)、范围查找(按分数区间ZRANGEBYSCORE
)。
-
6.Redis为什么快?
-
内存操作: 数据存储在内存,读写速度极快。
-
单线程模型:
-
避免多线程上下文切换和锁竞争开销。
-
I/O多路复用 (epoll/kqueue): 单线程高效处理大量并发连接。
-
-
高效的数据结构: 精心设计的底层数据结构(SDS, HashTable, SkipList, ZipList等)。
7.缓存与数据库的数据一致性
问题本质: 如何保证缓存中的数据和数据库中的数据在更新操作后保持一致
-
延迟双删(Double Delete): (Cache-Aside的增强)
-
更新数据库。
-
删除缓存。
-
等待一小段时间 (比如几百毫秒,根据业务主从延迟估算)。
-
再次删除缓存。
-
目的:清除在步骤1-2期间可能被其他读请求写入缓存的旧数据。
-
步骤 操作 目的 注意事项 1 更新数据库 确保数据持久化 更新主库 2 首次删除缓存 清除旧缓存 立即执行 3 等待一段时间 等待主从同步完成 时间需根据业务主从延迟设置 4 再次删除缓存 清除期间可能写入的旧数据 确保最终一致性
-
-
为什么需要二次删除:避免脏读回填
-
脏读回填问题图示
-
订阅数据库变更日志(Binlog): (如Canal, Debezium)
-
通过监听数据库的变更日志(如MySQL的Binlog),将变更事件发布到消息队列(如Kafka)。
-
独立的消费者程序消费消息,更新或删除Redis中的缓存。
-
优点: 解耦应用,通用性强,能保证最终一致性。
-
缺点: 架构复杂,引入新组件,延迟比Cache-Aside高。
-
-
关键点:
-
强一致性(CP)代价很高,通常选择最终一致性(AP)。
-
选择删除缓存而不是更新缓存,能有效避免复杂的并发更新时序问题(如两个并发写操作更新DB和缓存的顺序错乱导致脏数据)。
-
在高并发写场景下,任何策略都可能存在短暂不一致,需要业务容忍度或结合其他机制。
-