1. 缓存穿透
查询一个根本不存在的数据,缓存层查询为空,则查询存储层。如果存储层的查询结果也为空,则不会写入数据到存储层。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义
- 缓存穿透的基本原因有两点:
- 不严格的参数校验:
我们一般设计表 id 字段基本 > 0,但是一直用一个 < 0 的参数去请求,而我们又没有做严格的参数校验,所以请求不会被拦截,且每次都能绕开 Redis 直接打到数据库。 - 一些恶意攻击、爬虫等造成大量的空命中。
- 解决缓存穿透
- 缓存空对象
如果存储层仍然没有查询结果,则将空对象保留到缓存中,后续请求再次访问则直接返回缓存中的空对象。
但是缓存空对象会引发两个问题:
第一,对空值做缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是大量的攻击请求,问题会更严重),一个比较有效的方法是针对这类空数据设置一个较短的过期时间,让其自动剔除。
第二,如果存储层进行添加数据操作,那么应该和更新操作一样进行缓存的删除,不然那此段时间就会出现缓存层和存储层数据的不一致。 - 布隆过滤器拦截
这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少,存在一定的误判率。
2. 缓存击穿
缓存击穿是指一个热点 Key,大量请求集中对这个 key 进行访问,当这个 Key 在失效的瞬间,大量的请求就会穿破缓存,直接请求数据库,可能将数据库打死。
解决缓存击穿:
- 使用分布式互斥锁
查询缓存结果为空时,不是立刻去查询 DB,而是先针对这个 key 上分布式锁,上锁成功后,才能查询 DB 并设置缓存。伪代码如下
public String get(key) {
String value = redis.get(key);
// 代表缓存值过期
if (value == null) {
// 设置 1min 的超时
boolean res = lock.tryLock(60)
// 代表设置成功
if (res) {
// 这个时候其他线程可能已经成功设置缓存了
String value = redis.get(key);
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
// 重试
sleep(50);
get(key);
}
} else {
return value;
}
}
- 使热点 key “永不过期”
这里的 “永不过期” 包含两层意思:
(1) 从 redis 上看,确实没有设置过期时间,这就保证了不会出现热点 key 过期问题,也就是 “物理” 不过期。
(2) 从功能上看,我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程再进行缓存的构建,也就是 “逻辑” 过期。从项目反馈上来看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是旧数据,但是对于一般的互联网功能来说这个是可以忍受。
3. 缓存雪崩
由于缓存层承载着大量请求,有效地保护了存储层。但是如果缓存层由于某些原因不能提供服务,比如同一时间缓存数据大面积失效,那一瞬间所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
最常用的手段就是将缓存的失效时间分散开,避免缓存的集体失效。
4. 小结
缓存是提升性能的利器,所以保证缓存的高可用是十分有必要的。
Sentinel 和 Redis Cluster都实现了高可用。目前公有云厂商提供的缓存服务可用性非常高,对比自建的缓存服务性能和稳定性都要高上不少。当然啦,缺点就是贵😝😝😝
其次很重要的一点,我们一定要对后端 DB 的查询进行限流,防止数据库被冲垮。