Redis 八股文 —— 缓存
· 10 min read ·
—
内存淘汰
- 先进先出 FIFO (First In First Out):淘汰最早进入缓存的数据。实现简单,但不考虑访问频率,可能淘汰热点数据。
- 最近最少使用 LRU (Least Recently Used):淘汰最长时间未使用的数据。经典实现是「哈希表 + 双向链表」,但 Redis 采用了近似 LRU 算法以节省内存。
- 最不经常使用 LFU (Least Frequently Used):淘汰访问频率最低的数据 (Redis 4.0+ 支持)。能更好地保留长期热点数据,避免突发流量导致的热点被淘汰。
过期删除
- 惰性删除:每次访问键时检查是否过期,若过期则立即删除。
- 优点:CPU 开销极小,只在访问时才检查。
- 缺点:如果过期键不再被访问,会永久占用内存(“僵尸键”)。
- 定期删除:每隔固定时间(默认 100ms)随机抽取部分过期键进行清理。
- 优点:主动清理内存。
- 缺点:两次扫描之间,过期键仍可能被访问 → 可能返回已过期但尚未被删除的数据。
概念区分:过期删除 vs 内存淘汰
| 特性 | 过期键删除策略 | 内存淘汰策略 |
|---|---|---|
| 核心目标 | 清理已到过期时间的键 | 内存达到上限时主动淘汰键 |
| 触发条件 | 键设置了过期时间且已到期 | 已用内存 ≥ maxmemory 配置值 |
| 处理范围 | 仅处理设置了过期时间的键 | 可配置为全量键或仅过期键 |
一句话总结:过期删除是“时间到了就删”,内存淘汰是“内存满了必须删”。
缓存一致性
缓存一致性是指:数据库中的数据与缓存中的数据保持一致。当数据发生变更时,如何保证缓存不会返回过期的旧数据。
为什么会有缓存一致性问题?
产生原因:
- 写数据库和写缓存是两个独立的操作,不是原子的。
- 高并发下,操作顺序可能被打乱。
更新策略
Cache-Aside(旁路缓存)- 最常用
- 读:缓存未命中则读DB并回填。
- 写:先更新DB,再删除缓存。
为什么是删除缓存,而不是更新缓存?
| 方式 | 问题 |
|---|---|
| 更新缓存 | 可能更新一个复杂计算后的值;可能多次写无用缓存;并发更新可能覆盖 |
| 删除缓存 | 简单、安全;下次读时再重新加载最新值 |
Read-Through / Write-Through
应用程序将缓存作为主要数据源,由缓存代理所有读写操作。逻辑简单,但缓存层需要实现完整代理。
Write-Behind
只更新缓存,然后异步写DB(例如将更新步骤放到消息队列中)。写入吞吐量极高,但缓存故障可能导致数据丢失。
并发问题与解决方案
经典问题:先删缓存还是先写DB?
| 顺序 | 问题 |
|---|---|
| 先删缓存,再写DB | 写DB期间,读请求发现缓存未命中,读到旧数据并回填缓存 → 缓存变成脏数据 |
| 先写DB,再删缓存 | 写DB后、删缓存前,读请求读到旧缓存 → 短暂不一致 |
结论:先写DB再删缓存相对更安全,因为“读到旧缓存”的时间窗口更短。
并发写问题(延迟双删策略)
在高并发 + MySQL 主从读写分离的场景下,即使“先写DB,再删缓存”,也可能出现问题:
请求A(写主库) 请求B(读从库)
│ │
├─ 写主库成功 │
├─ 删除缓存(成功) │
│ ├─ 读缓存(未命中)
│ ├─ 读从库(主从延迟,读到旧数据) ❌
│ └─ 回填缓存(旧数据) ❌
问题根因:
- 主从复制存在延迟(毫秒到秒级)。
- 请求A删除缓存后,请求B因为主从延迟读到从库的旧数据。
- 请求B将旧数据回填到缓存,导致缓存变成脏数据。
解决方案:延迟双删
核心思想:删除两次缓存,第二次删除延迟执行,等待主从同步完成。
- 第一次删除:更新DB后立即删除缓存。
- 延迟等待:等待一段时间(略大于主从复制的延迟时间)。
- 第二次删除:再次删除缓存,以清除在此期间可能被回填的脏数据。
缓存击穿
定义:查询一个在数据库中存在,但在缓存中已过期的数据。
带来的问题:某个热点数据过期的一瞬间,大量并发请求同时穿透缓存,直接打到数据库,造成数据库压力骤增。
与其他概念的区分:
- 缓存穿透:查询一个不存在的数据。
- 缓存雪崩:大量缓存同时过期。
解决方案
- 互斥锁:只让一个线程去加载数据,其他线程等待。
- 设置热点数据永不过期:由后台异步线程主动更新缓存。
- 逻辑过期:缓存中不设置物理过期时间,而是存储一个逻辑过期字段,当发现逻辑过期时,返回旧数据并异步更新缓存。
缓存穿透
定义:查询一个数据库和缓存中都不存在的数据。
解决方案
- 在业务层提前拦截非法请求:如ID格式校验、ID范围校验。
- 缓存空对象:即使查询结果为空,也把这个“空结果”缓存起来(设置较短的过期时间)。
- 布隆过滤器:用一个极省内存的结构,快速判断一个 key 一定不存在。
布隆过滤器
布隆过滤器是一种概率型数据结构,它用一种非常省内存的方式,告诉你“一个元素一定不存在”或者“一个元素可能存在”。
核心原理:位数组 + 多个哈希函数
- 底层是一个位数组(bit array),每个位置初始为0。
- 添加:用 k个 独立的哈希函数计算k个位置,把这些位置都设为1。
- 查询:用同样的k个哈希函数计算位置。
- 所有位置都是1 → 元素可能存在(可能因哈希冲突被其他元素设置)。
- 有任何位置是0 → 元素一定不存在。
特点:不会漏掉存在的元素,但可能误判不存在的元素为“可能存在”。
缓存雪崩
定义:大量缓存集中在同一时间段过期,或者 Redis 服务宕机,导致所有请求直接打到数据库,造成数据库压力暴增甚至崩溃。
与其他概念的区分:
- 缓存击穿:单个热点数据过期(点问题)。
- 缓存雪崩:大量数据同时过期(面问题)。
解决方案
- 过期时间打散:给缓存过期时间加上随机偏移量,避免同时过期。
- 设置热点数据永不过期:由后台异步线程主动更新缓存。
- 缓存预热:在系统启动或大促前,提前把热点数据加载到缓存。
- Redis 高可用:使用主从+哨兵、Redis Cluster 等方案,防止 Redis 宕机。
- 多级缓存:本地缓存(如 Caffeine)+ Redis 分布式缓存,构建更稳固的防线。
总结对比
| 概念 | 问题描述 | 数据状态 | 解决方案核心 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 缓存和DB都没有 | 拦截不存在请求(缓存空值/布隆过滤器) |
| 缓存击穿 | 单个热点数据过期瞬间 | 缓存无,DB有 | 控制并发重建(互斥锁/永不过期) |
| 缓存雪崩 | 大量缓存同时过期/Redis宕机 | 批量数据同时失效 | 分散失效时间(随机偏移/预热/高可用) |