缓存穿透
缓存穿透,是指查询一个数据库不存在的数据。对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
处理方案
- 判断合法的请求:接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截。
- 当查询不存在时,也将结果保存在缓存中。但是这可能会存在一种问题:大量没有查询结果的请求保存在缓存中,这时我们就可以将这些请求的key设置得更短一些。
- 布隆过滤器(Bloom Filter):利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在。
缓存击穿
缓存击穿指的是:一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。
处理方案
-
多级缓存:结合redis缓存和本地缓存(ehcache、guava等)搭建多级缓存。将一些重要的热点数据存储到本地缓存中,有效缓解redis的压力。
-
用加锁或者队列的方式保证缓存的单线程(进程)写,在加锁方法内先从缓存中再获取一次,没有再查DB写入缓存。
public static String getData(String key) throws InterruptedException { //从Redis查询数据 String result = getDataByKV(key); //参数校验 if (StringUtils.isBlank(result)) { try { //获得锁 if (reenLock.tryLock()) { //去数据库查询 result = getDataByDB(key); //校验 if (StringUtils.isNotBlank(result)) { //插进缓存 setDataToKV(key, result); } } else { //睡一会再拿 Thread.sleep(100L); result = getData(key); } } finally { //释放锁 reenLock.unlock(); } } return result; }
缓存雪崩
缓存雪崩是指:大量缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩,导致数据库CPU和内存负载过高,甚至宕机。
缓存雪崩和缓存击穿情况比较相似,缓存击穿是一个高并发的热点key,在失效的瞬间,大量同个数据查询的请求直接落到数据库。而缓存雪崩是指大面积缓存在同一短时间内失效,导致大量各种数据请求落到数据库上。
缓存击穿是点击破,缓存雪崩是面击破。
举个栗子:如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了。
处理方案
-
缓存高可用:搭建高可用redis集群,主从+哨兵,redis cluster,避免全盘崩溃。
即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
-
限流&降级: hystrix 限流&降级。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合。降级的最终目的是保证核心服务可用,即使是有损的。
-
多级缓存:结合redis缓存和本地缓存(ehcache、guava等)搭建多级缓存。将一些重要的热点数据存储到本地缓存中,有效缓解redis的压力。
-
缓存失效时间点均匀分布:尽量让失效时间点均匀分布,设置不同的过期时间。在设置Redis键的过期时间时,加上一个随机数,这样可以避免。
-
Redis持久化和快速预热:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
总的来说缓存雪崩,可能是同一时间内大面积缓存时效,或者是缓存服务故障。大面积失效情况,可以在设置缓存有效期是增加一个随机值,使得缓存有效期分布均匀;或则设置多级缓存机制,多一层本地缓存机制,但是要考虑哪些数据适合放入本地缓存。缓存服务器故障情况,需要考虑缓存集群的高可用、缓存的持久化和快速预热。
总结
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
- 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。