缓存基础常见“学习”题总结
约 2270 字大约 8 分钟
缓存基础常见“学习”题总结
基础概念
1. 什么是缓存?为什么要用缓存?
- 定义:缓存(Cache)是存储数据的临时借地。
- 原因:
- 高性能:内存读写速度远快于磁盘(数据库)。
- 高并发:MySQL 单机 QPS 一般在 1k-4k,Redis 单机 QPS 可达 10w+。
- 减轻数据库压力:防止数据库被大量请求打崩。
2. 常见的缓存有哪些?
- 本地缓存:Guava Cache, Caffeine, Ehcache。速度最快,但无法分布式共享,占用 JVM 内存。
- 分布式缓存:Redis, Memcached。支持分布式共享,性能高,功能丰富。
3. Redis 和 Memcached 的区别?
- 数据类型:Redis 支持 String, List, Set, ZSet, Hash 等丰富类型;Memcached 只支持 String(二进制)。
- 持久化:Redis 支持 RDB 和 AOF;Memcached 不支持。
- 集群模式:Redis 原生支持 Cluster;Memcached 需客户端实现分片。
- 线程模型:Redis 单线程(6.0+ 多线程网络 I/O);Memcached 多线程。
缓存异常场景(重点)
4. 什么是缓存穿透?解决方案?
- 定义:查询根本不存在的数据,缓存无命中,数据库也无命中。请求直接穿透到数据库。
- 危害:黑客利用不存在的 Key 攻击,导致数据库压力过大宕机。
- 解决方案:
- 缓存空值:DB 没查到也在 Redis 存一个
null,设置短过期时间(如 30s)。 - 布隆过滤器(Bloom Filter):在访问缓存前,先判断 Key 是否存在。若判断不存在,则一定不存在,直接返回。
- 参数校验:接口层拦截非法参数(如 id < 0)。
- 缓存空值:DB 没查到也在 Redis 存一个
5. 什么是缓存击穿?解决方案?
- 定义:某个热点 Key(Hot Key)突然失效(过期),此时大量并发请求直接打到数据库。
- 危害:数据库瞬时压力激增。
- 解决方案:
- 互斥锁(Mutex Lock):查缓存未命中时,先获取锁(如
SETNX),获取成功再去查 DB 并回写缓存,失败则重试。 - 逻辑过期:Key 不设置 TTL,但在 Value 中包含过期时间。查询时判断是否过期,若过期则异步启动线程去更新缓存,当前请求返回旧值。
- 热点数据永不过期:物理不过期,由后台任务定时更新。
- 互斥锁(Mutex Lock):查缓存未命中时,先获取锁(如
6. 什么是缓存雪崩?解决方案?
- 定义:大量 Key 同时失效,或者 Redis 宕机,导致所有请求全部打到数据库。
- 解决方案:
- 随机过期时间:在原有的 TTL 基础上增加一个随机值(如 1-5 分钟),避免集体失效。
- Redis 高可用:搭建 Sentinel 或 Cluster 集群。
- 多级缓存:本地缓存(Caffeine) + 分布式缓存(Redis)。
- 限流降级:Sentinel/Hystrix 限制数据库访问流量,非核心业务降级。
7. 缓存穿透、击穿、雪崩的区别?
- 穿透:数据不存在(查不到)。
- 击穿:单个热点 Key 过期。
- 雪崩:大量 Key 过期或 Redis 挂了。
缓存一致性
8. 缓存和数据库的一致性如何保证?
- 读写策略(Cache Aside Pattern):
- 读:先读缓存 -> 命中返回;未命中读 DB -> 写入缓存 -> 返回。
- 写:先更新数据库,再删除缓存。
9. 为什么是删除缓存而不是更新缓存?
- 性能:如果写多读少,更新缓存频率高但没被读取,浪费性能。
- 复杂性:缓存数据可能经过复杂计算,直接更新成本高。
- 安全:并发更新可能导致脏数据(A 更新 DB -> B 更新 DB -> B 更新 Cache -> A 更新 Cache,导致 DB 是 B 的值,Cache 是 A 的值)。
10. 为什么先更新数据库再删除缓存?
- 先删缓存,后更新 DB:
- A 删缓存 -> B 读 DB(旧值) -> B 写缓存(旧值) -> A 更新 DB。导致缓存一直是旧值(脏数据)。
- 解决:延时双删(先删 -> 更新 DB -> 休眠 1s -> 再删)。
- 先更新 DB,后删缓存(推荐):
- 只有在“读写并发”且“缓存刚好失效”且“读 DB 耗时 > 写 DB 耗时”时才会出现脏数据,概率极低。
- 问题:删缓存失败怎么办?
- 解决:重试机制(MQ 消息队列)。
11. 什么是延时双删?
- 流程:
del(key) -> update(db) -> sleep(N) -> del(key)。 - 目的:确保在 sleep 期间进来的读请求(读到旧值写回缓存)能被第二次删除清理掉。
- 缺点:吞吐量降低(sleep),时间难以掌控。
12. 如何保证删除缓存成功?
- 消息队列:更新 DB 后,发送消息到 MQ,消费者消费消息去删除 Redis。如果失败则重试。
- 订阅 Binlog(Canal):监听 MySQL Binlog 变更,异步解析并删除 Redis。对业务代码无侵入。
缓存淘汰策略
13. Redis 的内存淘汰策略有哪些?
- noeviction(默认):不淘汰,内存满了直接报错(写操作)。
- allkeys-lru:所有 Key 中,淘汰最近最少使用的。
- volatile-lru:设置了过期时间的 Key 中,淘汰最近最少使用的。
- allkeys-random:所有 Key 中,随机淘汰。
- volatile-random:设置了过期时间的 Key 中,随机淘汰。
- volatile-ttl:淘汰剩余存活时间最短的。
- allkeys-lfu(4.0+):所有 Key 中,淘汰最不经常使用的(频率)。
- volatile-lfu(4.0+):设置了过期时间的 Key 中,淘汰最不经常使用的。
14. LRU 和 LFU 的区别?
- LRU (Least Recently Used):最近最少使用。基于时间。长期不用的被淘汰。
- LFU (Least Frequently Used):最不经常使用。基于频率。访问次数少的被淘汰。
15. 如何手写一个 LRU 算法?
- 数据结构:
HashMap+DoubleLinkedList(双向链表)。 - Java 实现:继承
LinkedHashMap,重写removeEldestEntry方法。
布隆过滤器
16. 什么是布隆过滤器(Bloom Filter)?
- 原理:利用多个 Hash 函数将数据映射到位数组(BitMap)中。
- 特点:
- 如果判断存在,可能不存在(误判)。
- 如果判断不存在,则一定不存在。
- 优点:空间效率极高,查询快。
- 缺点:有误判率,不支持删除(需使用计数布隆过滤器)。
17. 布隆过滤器的误判率怎么降低?
- 增加位数组长度(m)。
- 增加 Hash 函数个数(k)。
- 根据业务接受程度调整(如 0.01%)。
18. Redis 如何实现布隆过滤器?
- RedisModule:RedisBloom 插件。
- BitMap:利用 Redis 的
setbit/getbit手动实现。 - Redisson:Java 客户端 Redisson 内置了布隆过滤器实现。
其他
19. 缓存预热是什么?
- 定义:系统上线后,提前将热点数据加载到缓存中。
- 实现:
- 数据量小:项目启动时加载(
CommandLineRunner)。 - 数据量大:后台定时任务刷新。
- 数据量小:项目启动时加载(
20. 缓存降级是什么?
- 定义:当 Redis 出现故障或响应慢时,为了保证核心服务可用,暂时屏蔽缓存或返回默认值。
- 实现:Hystrix / Sentinel 熔断降级。
21. 多级缓存架构是怎样的?
- L1:进程内缓存(Ehcache/Caffeine),速度快,容量小。
- L2:分布式缓存(Redis),容量大,网络 I/O。
- L3:Nginx 缓存 / CDN。
- 流程:查 L1 -> 查 L2 -> 查 DB -> 回写 L2 -> 回写 L1。
22. Redis 到底是单线程还是多线程?
- 6.0 之前:核心网络模型和命令执行都是单线程。
- 6.0 之后:引入多线程处理网络请求(读写 Socket),但命令执行依然是单线程。
- 原因:Redis 瓶颈通常在网络 I/O,而不是 CPU。
23. Redis 单线程为什么这么快?
- 纯内存操作。
- IO 多路复用(Epoll):非阻塞 I/O。
- 单线程:避免了上下文切换和锁竞争。
- 高效数据结构(SDS, Hash, SkipList)。
24. 什么是热 Key(Hot Key)?如何解决?
- 定义:访问频率极高的 Key(如微博热搜)。
- 危害:流量集中在一个分片,导致该分片网卡/CPU 打满。
- 解决:
- 本地缓存:将热 Key 缓存在应用服务器本地(Caffeine)。
- 读写分离:主从架构,增加 Slave 分担读压力。
- Key 拆分:将
key复制为key1, key2...,分散到不同分片。
25. 什么是大 Key(Big Key)?如何解决?
- 定义:Value 很大(String > 10KB, List/Hash 元素 > 5000)。
- 危害:阻塞网络,阻塞 Redis 线程(删除时)。
- 解决:
- 拆分:将大 Hash 拆分为多个小 Hash。
- 压缩:对 Value 进行压缩。
- 惰性删除:
UNLINK命令异步删除。
26. Redis 怎么实现消息队列?
- List:
lpush+rpop(先进先出)。 - Pub/Sub:发布订阅(消息无法持久化,无 ACK)。
- Stream(5.0+):支持消费者组、持久化、ACK,类似 Kafka。
27. Redis 怎么实现延迟队列?
- ZSet:
score存时间戳,value存消息。 - 消费:
zrangebyscore轮询获取到期的消息。
28. 缓存满了怎么办?
- 触发内存淘汰策略(Eviction Policy)。
29. 怎么发现热 Key?
- Redis 4.0+:
--hotkeys命令。 - 客户端统计:在代码中做计数。
- 抓包分析:分析 TCP 流量。
30. 怎么发现大 Key?
- Redis:
--bigkeys命令。 - SCAN:扫描全库,计算长度。
- RDB 分析工具:rdb-tools。
