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 提供的相关类库即可。
com.google.guavaguava25.1-jre
public class Test1 { private static int size = 1000000; //定义错误率 private static BloomFilterbloomFilter = 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 热点永不过期
不过期自然也就没有这个问题。