真正理解雪崩、击穿、穿透的方法只有一个:
理解缓存系统的运行规律
一、为什么需要缓存?
想象你做了一个简单的博客网站。
用户访问文章时,服务器会做这件事:
查询数据库 → 返回数据
如果有 10000 个用户同时访问:
数据库就会变成这样:
大量查询请求 → 数据库 CPU 爆满
数据库是整个系统最贵、最慢的部分。
所以工程师会加一层东西:
缓存
系统结构变成:
用户
↓
服务器
↓
缓存(Redis)
↓
数据库(MySQL)
访问流程是:
1 先查缓存
2 缓存没有再查数据库
3 查到数据后写入缓存
这样大部分请求都会停在缓存层。
数据库压力就会小很多。
二、缓存系统的一个核心特点
缓存不是完整数据
因为缓存通常:
1 有过期时间
2 内存容量有限
所以缓存一定会出现:
有些数据存在 有些数据不存在
正是这个特点,导致了后面三个经典问题:
缓存穿透 缓存击穿 缓存雪崩
它们本质上都是:
缓存没有挡住请求,导致请求直接冲击数据库
只是发生的方式不同。
三、什么是缓存穿透?
想象一个请求:
java
/article/999999
但数据库里根本没有这篇文章。
流程会变成:
Redis:没有 MySQL:没有
如果用户不断访问这个不存在的数据:
请求
↓
Redis(没有)
↓
MySQL(没有)
每次都会查询数据库。
这就叫:
缓存穿透
本质是:
查询不存在的数据 缓存无法拦截
穿透为什么危险?
如果有人恶意攻击:
每秒10万请求 访问不存在的ID
那么:
所有请求都会打到数据库 数据库就会崩溃。
如何解决缓存穿透?
1. 缓存空值
当数据库发现数据不存在时:
也写入缓存。
例如:
key: article:999999 value: null
设置短过期时间:5分钟
这样下次访问时:
Redis直接返回null
数据库就不会再被访问。
2. 布隆过滤器
在系统入口建立一个:
布隆过滤器
它的作用是:
快速判断某个ID是否可能存在
如果判断:
一定不存在
就直接拒绝请求。
数据库完全不会被访问。
四、什么是缓存击穿?
缓存击穿发生在:
热点数据
比如一篇特别火的文章:
article:1001
每天可能有:几万次访问
某一刻:
缓存刚好过期。
article:1001 缓存失效
突然:5000个用户同时访问
所有请求都会:
同时查询数据库 数据库瞬间压力暴涨
这就是:
缓存击穿
本质是:
热点数据缓存失效
大量请求同时访问数据库
如何解决缓存击穿?
最常见的方法是:
互斥锁
意思是:
当缓存失效时:
只允许一个线程查询数据库。
流程:
1 第一个请求获得锁
2 查询数据库
3 写入缓存
4 释放锁
其他请求:
等待缓存恢复
这样数据库只会被查询一次。
五、什么是缓存雪崩?
缓存雪崩发生在:
大量缓存同时过期
比如系统中有:
100万个缓存key
如果这些缓存设置了:
同样的过期时间
例如:1小时
那么一小时后:
所有缓存一起失效
此时所有请求都会:
直接访问数据库 数据库瞬间崩溃。
这就是:
缓存雪崩
本质是:
缓存集体失效
数据库压力瞬间爆炸
如何解决缓存雪崩?
- 方法一:随机过期时间
不要让缓存同时过期。
例如:
3600 + random(300)
意思是:
1小时 + 随机5分钟
这样缓存过期时间就会分散。
- 方法二:多级缓存
增加缓存层:
本地缓存
Redis缓存
数据库
系统变成:
用户
↓
本地缓存
↓
Redis
↓
MySQL
这样压力会被多层分散。
六、数据库和缓存如何保持一致?
这是缓存系统里最难的问题。
因为系统有两个数据源:
数据库 缓存
如果更新顺序不对,就会出现:
数据不一致
例如:
数据库更新了,但缓存还是旧数据。
常见错误做法
错误流程:
更新数据库
更新缓存
如果更新缓存失败:
数据库新数据
缓存旧数据
就出现不一致。
最常见解决方案:先更新数据库,再删除缓存
工程上最常见的方式是:
更新数据库
删除缓存
流程:
update MySQL
delete Redis
为什么要删除而不是更新?
因为:
缓存可以重新生成
当下次请求到来时:
缓存不存在
→ 查询数据库
→ 重新写缓存
数据自然就一致了。
七、完整流程总结
读取数据:
查Redis
↓
存在 → 返回
↓
不存在
↓
查MySQL
↓
写入Redis
↓
返回
更新数据:
更新MySQL
↓
删除Redis
八、理解缓存系统的核心本质
如果把所有技术细节都抽象掉,缓存系统的本质其实只有一句话:
缓存是用内存空间换取访问时间的一种系统设计。
而缓存问题(雪崩、击穿、穿透)本质都是:
缓存没有成功拦截请求
导致:
请求直接冲击数据库
所以缓存设计的核心目标是:
1 尽量让请求停在缓存层
2 防止请求洪水冲向数据库
3 保证数据最终一致