1. 缓存穿透
1.1 概念
请求的 key 在缓存和数据源中都不存在,就会导致每次请求都访问到数据源,失去了缓存的意义。
1.1.1 示例代码
@Override
public Goods searchArticleById(Long goodsId){
Object object = redisTemplete.opsForValue().get(String.valueOf(goodsId));
//缓存查询命中
if(object != null){
return Goods(object);
}
//从数据源中查询
Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
if(goods != null){
//将查询结果放入缓存
redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);
}
return goods;
}
1.2 解决方案
1.2.1 缓存空值
如果一个查询没有从数据源获取到数据,不管它是真的不存在还是存在故障,我们都把这个空结果进行缓存,但是要设置一个较短的过期时间。这样在短时间内,再次查询就不会继续去访问数据源了。
1.2.1.1 实例代码
@Override
public Goods searchArticleById(Long goodsId){
Object object = redisTemplete.opsForValue().get(String.valueOf(goodsId));
//缓存查询命中
if(object != null){
return Goods(object);
}
//从数据源中查询
Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
if(goods != null){
//将查询结果放入缓存
redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);
}else{
//缓存空值
redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.SECONDS);
}
return goods;
}
1.2.2 布隆过滤器
将所有可能请求的数据放入一个足够大 bitmap 中,一个不存在的请求数据就会被拦截到,避免对数据源的冲击。
1.2.2.1 什么是布隆过滤器
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为:O (n), O (log n), O (n/k)。
布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。
1.2.2.2 布隆过滤器的实现
在实际应用当中,我们不需要自己去实现 BloomFilter。可以使用 Guava 提供的相关类库即可。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
public class Test1 {
private static int size = 1000000;
//定义错误率
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);
public static void main(String[] args) {
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
long startTime = System.nanoTime(); // 获取开始时间
//判断这一百万个数中是否包含29999这个数
if (bloomFilter.mightContain(29999)) {
System.out.println("命中了");
}
long endTime = System.nanoTime(); // 获取结束时间
System.out.println("程序运行时间: " + (endTime - startTime) + "纳秒");
}
}
布隆过滤器本身存在一定的不准确性。
1.2.3 接口限流与熔断、降级
在一定时间流量超出预期后,拒绝预期的访问,给予用户友好提示。
2. 缓存击穿
2.1 概念
某一个 key 访问流量非常大,大并发集中对这个 key 进行发问,当这个 key 失效的瞬间,持续的大并发直接落到了数据源上,对数据源造成冲击。
2.2 解决方案
2.2.1 热点键永不过期
如果可以话,热点键不设置过期时间,这样也就不存在击穿的问题。
2.2.2 互斥锁
当缓存失效的时候,使用互斥锁来让请求进行排队。这样并没有提供并发量,只是解决了可能会造成的数据源的压力。
2.2.2.1 示例代码
@Override
public Goods searchArticleById(Long goodsId){
Object object = CacleUtils.get(String.valueOf(goodsId));
//缓存查询命中
if(object != null){
return Goods(object);
}
//先尝试获取一把分布式锁
Goods goods = null;
try{
Boolean result = ReenLock.tryLock(key_mutex,requestId,60000);
if(result){
//从数据源中查询
goods = goodsMapper.selectByPrimaryKey(goodsId);
if(goods !=null){
CacheUtils.set(String.valueOf(goodsId),goods,60000);
}
}else{
//稍后再去尝试
Thread.sleep(100);
result = searchArticleById(goodsId);
}
}finally{
ReenLock.unLock(key_mutex,requestId);
}
return goods;
}
3. 缓存雪崩
3.1 概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
3.2 解决方案
3.2.1 加锁排队
使用互斥锁,让来进来的请求进行排队处理。
3.2.2 不同过期时间
既然缓存雪崩时同一时间点,大量的key过期,导致请求落到数据源上,那就尽量让不同的键设置不同的过期时间,尽量让缓存失效的时间分布均匀。
3.2.3 双层缓存策略
C1 为原始缓存,C2 为拷贝缓存,C1 失效时,可以访问 C2,C1 缓存失效时间设置为短期,C2 设置为长期。
3.2.4 数据预热
解决冷启动造成的雪崩,可以提前预估热点数据,将相关的缓存数据直接加载到缓存系统,避免冷启动。
3.2.5 热点永不过期
不过期自然也就没有这个问题。