目录
[使用 Redis 作为缓存](#使用 Redis 作为缓存)
[缓存预热(Cache preheating)](#缓存预热(Cache preheating))
[缓存穿透(cache penetration)](#缓存穿透(cache penetration))
[缓存雪崩(Cache avalanche)](#缓存雪崩(Cache avalanche))
[缓存击穿(Cache breakdown)](#缓存击穿(Cache breakdown))
什么是缓存
缓存(cache) 是计算机中的一个经典概念,在很多场景中都会涉及到,其核心思路是:将高频访问、计算成本高、变化不频繁 的数据,临时存储在访问速度更快 的介质中,以减少对慢速数据源的请求,从而提升系统响应速度、降低后端压力
而这里的访问速度更快 ,是一个相对的概念:
对于硬件的访问速度来说,通常是:CPU 寄存器 > 内存 > 硬盘 > 网络
那么,相比于网络,硬盘的访问速度更快,就可以使用硬盘作为网络的缓存 ;相比于硬盘,内存的访问速度更快,就可以使用内存作为硬盘的缓存
可以看到,缓存是一种用空间换时间 的策略,但往往访问速度更快的设备,成本越高,存储空间越小,因此,在大部分时候,缓存只能放一些热点数据(也就是访问频繁的数据)
然而,对于大部分的情况,都满足 "二八定律 ":在任何一组事物中,最重要的只占其中约 20%,其余 80% 尽管是多数,却是次要的
也就是说 20% 的热点数据,就能够应对 80% 的访问场景
因此,我们只需要将这些少量的热点数据缓存起来,就可以应对大多数场景,从而明显的提升系统性能
使用 Redis 作为缓存
在很多场景中,我们会使用关系型数据库(如 MySQL )来存储数据
关系型数据库的功能强大,但由于它在强一致性、复杂查询、数据安全等方面上做了深度优化,代价是牺牲了部分极致吞吐与低延迟,因此性能不高
因此,若访问关系型数据库的并发量较高,就会导致数据库压力较大,也就容易使数据库服务器宕机
那么,如何让关系型数据库承担更大的并发量呢?
我们可以从以下两个方面考虑:
开源:引入更多的机器,部署更多的数据库实例,构成数据库集群
节流:引入缓存,缓存经常访问的热点数据,从而降低直接访问数据库的请求数量
而相比于关系型数据库,Redis 的访问速度更快:
Redis 的数据存储在内存中,访问内存比访问硬盘快得多
Redis 只支持简单的 key-value 存储,不涉及复杂查询的限制规则
因此 Redis 是作为关系型数据库缓存的常见方案:

当客户端发起查询请求时,业务服务器会先从 Redis 上查询数据:
若在 Redis 上查询成功,直接获取数据,就不用再访问 MySQL 了
若在 Redis 上查询失败,则继续在 MySQL 上查询数据
若需要写数据,则需要修改 MySQL 上的数据,Redis 不能提高性能
缓存的更新策略
在实际场景中,我们如何确定哪些数据是热点数据呢?也就是说,我们应该缓存哪些数据到Redis上呢?
对于需要存储的热点数据,我们可以进行定时生成 或实时生成
我们先来看定时生成
定时生成
定时生成 :每隔一定的周期 (如 一天、一周、一个月),对访问的数据频次进行统计,挑选出访问频次最高的前 N% 数据,并将其进行缓存
例如,我们可以使用定时任务,按照固定周期(如一天),定期统计查询日志,从而得到热点数据
但是这种方式的实时性较低 ,对于突发情况无法很好的处理,例如,某个时刻,某条数据的访问频次突然增大,但其未进行缓存,就会导致数据库压力增大
实时生成
先为缓存设置容量上限 (通过 Redis 配置文件的 maxmemory参数指定)
接下来,对于用户的查询:
先在 Redis 中进行查询,若查询成功,直接返回
若在 Redis 中查询失败,就从数据库中查,并将查询结果写入 Redis 中,以便下次可直接查询 Redis
若缓存达到上限,就会触发缓存淘汰策略,将一些 "相对不那么热门" 的数据淘汰掉
而常见的淘汰策略有:
FIFO(First In First Out,先进先出) :将缓存中存放时间最久的数据淘汰掉
LRU(Least Recently Used,淘汰最久未使用) :记录每个 key 的最近访问时间,将最近访问时间最早的 key 淘汰掉
LFU(Least Frequently Used,淘汰访问次数最少) :记录每个 key 最近一段时间的访问次数,将访问次数最少的淘汰掉
Random(随机淘汰 ):从所有 key 中随机选一个淘汰掉
Redis 也内置了淘汰策略,与常见的淘汰策略基本一致,但会针对带有过期时间的key 和全部key 分别处理:
| 策略 | 作用范围 | 淘汰算法 | 适用场景 |
|---|---|---|---|
| noeviction(默认) | 全部 Key | 不淘汰,直接拒绝写入(返回 -OOM command not allowed) | 纯持久化存储、不允许丢数据的场景 |
| allkeys-lru | 全部 Key | 最近最少使用(Least Recently Used) | 纯缓存场景,热点数据保留(最常用) |
| volatile-lru | 仅带 EXPIRE的 Key | LRU | 缓存与持久化数据混存,只淘汰有过期时间的 Key |
| allkeys-lfu | 全部 Key | 最不经常使用(Least Frequently Used) | 访问频率分布稳定、长周期热点(如排行榜、内容库) |
| volatile-lfu | 仅带 EXPIRE的 Key | LFU | 混存场景 + 频率特征明显 |
| allkeys-random | 全部 Key | 随机淘汰 | 访问模式完全均匀,或 benchmark 测试 |
| volatile-random | 仅带 EXPIRE的 Key | 随机淘汰 | 同 allkeys-random,但只动过期 Key |
| volatile-ttl | 仅带 EXPIRE的 Key | 剩余 TTL 最短者优先 | 时间敏感型数据(如验证码、临时令牌) |
典型问题
缓存预热(Cache preheating)
当使用 Redis 作为 MySQL 缓存时,若 Redis 刚启动,或 Redis 中大批 key 失效,此时 Redis 相当于是空的,没有什么缓存数据,此时缓存命中率极低,导致 MySQL 的访问量增大,从而增大其压力
因此,需要提前将热点数据准备好,直接写入 Redis 中,这些热点数据不一定"准确",但能帮助 MySQL 抵挡大部分请求,随着运行时间的推移,缓存中的热点数据会自动调整,从而适应当前情况
缓存穿透(cache penetration)
缓存穿透 :请求的数据在缓存中不存在,在数据库中也不存在。由于缓存未命中,请求直接穿透到数据库,若并发量大,就会导致数据库承担请求增多,压力增大
为什么会出现缓存穿透呢?
-
参数校验缺失:前端传入空字符串、越界 ID、非法格式等数据时,业务层未拦截无效请求
-
数据已删除:商品下架/用户注销后,缓存未清理,此时历史请求仍携带旧 Key,数据库中已无对应记录
-
恶意攻击/爬虫:攻击者故意构造不存在的 Key,绕过缓存直打数据库
那么,该如何解决缓存穿透问题呢?
-
加强参数校验:对要查询的参数进行严格的合法性校验,拦截参数不符合要求的请求
-
缓存空对象(cache null):针对数据库上不存在的数据,也将其缓存到 Redis中,避免后续频繁访问
3.布隆过滤器(Bloom Filter)筛选 :布隆过滤器结合哈希 + 位图的思想,使用较少的空间,存储所有存在的 key,通过布隆过滤器先判断 key 是否存在,若不存在,则直接拦截
缓存雪崩(Cache avalanche)
缓存雪崩 :大量缓存数据 在同一时间过期失效,或缓存服务整体宕机,导致请求瞬间全部穿透到数据库,引发数据库压力骤增甚至崩溃,即缓存层的集体失守,使数据库在极短时间内承受远超其处理能力的请求洪峰
为什么会出现缓存雪崩呢?
-
Redis 中key批量过期:大批 Key 设置相同过期时间,导致整点同时过期
-
Redis 服务不可用:Redis 主从节点全挂,或集群中超过一半的主节点都挂了,导致缓存服务不可用
那么,该如何解决现缓存雪崩问题呢?
-
在设置 key 的过期时间时添加随机因子,让过期时间随机化
-
部署高可用的 Redis集群 ,并完善监控报警系统
缓存击穿(Cache breakdown)
缓存击穿 :某个热点 Key缓存过期失效的瞬间,大量并发请求同时涌入,全部穿透缓存直接打到数据库,导致数据库瞬间负载飙升
与"雪崩"(大面积过期/宕机)和"穿透"(查无此数据)不同,击穿聚焦于高并发场景下的单个/少数热点 Key 失效引发问题
那么,该如何解决现缓存击穿问题呢?
-
基于统计方式发现的热点key,并设置其永不过期
-
进行必要的服务降级,如访问数据库时使用分布式锁,限制同时请求数据库的并发数