带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩

在这里插入图片描述

文章开始之前先做个找工作的介绍吧,由于这次疫情影响,我一个UI朋友的公司破产之后他现在处于找工作阶段,一直没有找到自己合适的,三年工作经验左右,坐标深圳,如果有招UI的朋友可以联系我。

作品: http://yiming.zcool.com.cn/

缓存是互联网开发中必不可少的一部分,它能降低我们数据库的并发数,提高我们系统的性能,比如我们经常使用的redis、emCached等等,其中redis应该是大部分的人选,为什么?因为速度快,易上手,是很多开发者的首选,但是缓存同样存在这问题,如果使用的不恰当,也可能会造成非常严重的后果,这时候你可能就会有疑问,缓存只是存储一些数据而已,怎么会造成严重的后果呢?下面我就带大家一起来分析分析。

什么是缓存

缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。

比如我们的redis、他就是缓存中比较常见的一种,他的并发读写能力能达到10w/s左右的速度,这个速度是相当不错的,相对于传统的数据存储来说,比如数据库,快了不知道多少倍,传统的数据库(mysql)操作的都是磁盘,而redis操作的是内存(ram),所以他们的速度肯定是没法比较的,由于传统数据库的读写较慢,所以并发较高的时候就会造成性能瓶颈问题,这也是为什么需要引入缓存的原因之一。

人在地上走,锅从天上来

我是一个快乐的程序狗,每天最快乐的事情就是codding,最大的愿望就是能准时6点下班,然后回家,但是今天肯定是走不了了,现在是17:30,我们的测试小哥哥给我提了一个很诡异的bug,难受啊,我的准时下班梦,但是作为一个程序狗,肯定都有着一颗和bug战斗到底的决心,究竟是什么bug呢?

bug是这样的:并发请求订单信息,没过几秒就抛出系统错误。这个bug看着没几个字,但是一看就知道不好解决,尤其是像这种并发bug,能让人瞬间白了头,随后我找到了测试,让他们复现了这个神秘的bug,而我也找到了产生这个bug的来源,并且快速的修复了他,到底是什么问题呢?是因为小明(同事)在编写代码的时候考虑的不是很周全导致的,所以开发一定要想仔细了再动手,否则吃亏的就是自己啊。

缓存穿透

什么是缓存穿透

缓存穿透指的是:同一时刻,大量的并发请求数据库中不存在的信息,他既不会命中缓存,也不会命中数据库,但是他会查找数据库。

扫描二维码关注公众号,回复: 11063338 查看本文章

上面的bug也是因为它产生的,测试的小哥哥查询的订单都是数据库不存在的,所以这个时候这些并发请求都不会命中缓存(redis),将直达数据库(mysql),由于大量的并发请求到达数据库,而数据库承受不住这么高的并发,从而导致数据库奔溃,这就是缓存穿透

重现bug

1.新建数据表:订单表,结构如下:
在这里插入图片描述
2.编写测试代码

OrderBo.java

package com.ymy.bo;

import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;

@Data
public class OrderBo implements Serializable {

<span class="token comment">/**
 * 自增id
 */</span>
<span class="token keyword">private</span> Long id<span class="token punctuation">;</span>

<span class="token comment">/**
 *订单编号
 */</span>
<span class="token keyword">private</span> Long orderCode<span class="token punctuation">;</span>

<span class="token comment">/**
 *订单价格
 */</span>
<span class="token keyword">private</span> BigDecimal orderPrice<span class="token punctuation">;</span>

<span class="token comment">/**
 *商品名称
 */</span>
<span class="token keyword">private</span> String  peoductName<span class="token punctuation">;</span>

<span class="token comment">/**
 *创建时间
 */</span>
<span class="token keyword">private</span> String createTime<span class="token punctuation">;</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

OrderController.java

package com.ymy.controller;

import com.ymy.bo.OrderBo;
import com.ymy.service.OrderService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

<span class="token keyword">private</span> OrderService orderService<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token function">OrderController</span><span class="token punctuation">(</span>OrderService orderService<span class="token punctuation">)</span><span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>orderService <span class="token operator">=</span> orderService<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token annotation punctuation">@RequestMapping</span><span class="token punctuation">(</span>value <span class="token operator">=</span> <span class="token string">"/detail"</span><span class="token punctuation">,</span>method <span class="token operator">=</span> RequestMethod<span class="token punctuation">.</span>GET<span class="token punctuation">)</span>
<span class="token keyword">public</span> OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span><span class="token annotation punctuation">@RequestParam</span><span class="token punctuation">(</span><span class="token string">"id"</span><span class="token punctuation">)</span> Long id<span class="token punctuation">)</span><span class="token punctuation">{</span>

    <span class="token keyword">return</span> orderService<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

OrderService.java

package com.ymy.service;

import com.ymy.bo.OrderBo;
import com.ymy.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderService {

<span class="token keyword">private</span> RedisTemplate redisTemplate<span class="token punctuation">;</span>

<span class="token keyword">private</span> OrderMapper orderMapper<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token function">OrderService</span><span class="token punctuation">(</span>RedisTemplate redisTemplate<span class="token punctuation">,</span>OrderMapper orderMapper<span class="token punctuation">)</span><span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>redisTemplate <span class="token operator">=</span> redisTemplate<span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>orderMapper <span class="token operator">=</span> orderMapper<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">/**
 * 通过id查询订单详情
 * @param id
 * @return
 */</span>
<span class="token keyword">public</span> OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span>Long id<span class="token punctuation">)</span> <span class="token punctuation">{</span>

    <span class="token comment">//缓存中查询词词订单</span>
    OrderBo  orderBo <span class="token operator">=</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span><span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null <span class="token punctuation">)</span><span class="token punctuation">{</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"缓存中查询到了信息,直接返回:{}"</span><span class="token punctuation">,</span>orderBo<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> orderBo<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往数据库查询"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    orderBo <span class="token operator">=</span>  orderMapper<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span><span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null <span class="token punctuation">)</span><span class="token punctuation">{</span>
       <span class="token comment">//将数据保存到数据库,有效时间一小时</span>
       redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span>orderBo<span class="token punctuation">,</span><span class="token number">3600</span><span class="token punctuation">,</span>TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>
   	   log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"数据已经存入缓存"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> orderBo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

OrderMapper.java

package com.ymy.mapper;

import com.ymy.bo.OrderBo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface OrderMapper {
/**
* 通过订单id查询订单信息
* @param id
* @return
*/

@Select(" select id,order_code as orderCode,order_price as orderPrice,peoduct_name as peoductName,create_time as createTime from orders where id = #{id} ")
OrderBo getDetail(Long id);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

RedisConfig.java

package com.ymy.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

<span class="token annotation punctuation">@Bean</span>
<span class="token keyword">public</span> RedisTemplate<span class="token generics function"><span class="token punctuation">&lt;</span>Object<span class="token punctuation">,</span> Object<span class="token punctuation">&gt;</span></span> <span class="token function">redisTemplate</span><span class="token punctuation">(</span>RedisConnectionFactory redisConnectionFactory<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    RedisTemplate<span class="token generics function"><span class="token punctuation">&lt;</span>Object<span class="token punctuation">,</span> Object<span class="token punctuation">&gt;</span></span> redisTemplate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">RedisTemplate</span><span class="token operator">&lt;</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setConnectionFactory</span><span class="token punctuation">(</span>redisConnectionFactory<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 使用Jackson2JsonRedisSerialize 替换默认序列化</span>
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Jackson2JsonRedisSerializer</span><span class="token punctuation">(</span>Object<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    ObjectMapper objectMapper <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ObjectMapper</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    objectMapper<span class="token punctuation">.</span><span class="token function">setVisibility</span><span class="token punctuation">(</span>PropertyAccessor<span class="token punctuation">.</span>ALL<span class="token punctuation">,</span> JsonAutoDetect<span class="token punctuation">.</span>Visibility<span class="token punctuation">.</span>ANY<span class="token punctuation">)</span><span class="token punctuation">;</span>
    objectMapper<span class="token punctuation">.</span><span class="token function">enableDefaultTyping</span><span class="token punctuation">(</span>ObjectMapper<span class="token punctuation">.</span>DefaultTyping<span class="token punctuation">.</span>NON_FINAL<span class="token punctuation">)</span><span class="token punctuation">;</span>

    jackson2JsonRedisSerializer<span class="token punctuation">.</span><span class="token function">setObjectMapper</span><span class="token punctuation">(</span>objectMapper<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 设置value的序列化规则和 key的序列化规则</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setKeySerializer</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">StringRedisSerializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setValueSerializer</span><span class="token punctuation">(</span>jackson2JsonRedisSerializer<span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setHashKeySerializer</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">StringRedisSerializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setHashValueSerializer</span><span class="token punctuation">(</span>jackson2JsonRedisSerializer<span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">afterPropertiesSet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> redisTemplate<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

上面的代码实现的功能很简单,通过订单id查询订单详情,不过查询的循序是先查缓存,如果缓存没有数据,在查询数据库,大致流程图如下:
在这里插入图片描述
这个过程很简单,看上去没有什么问题,如果你仔细观察的话就会发现一个致命的问题,就是刚刚说的缓存穿透问题,我们来做个实验。

我在数据库提前添加了一条数据,信息如下:
在这里插入图片描述
正常情况:查询id等于1的订单信息。

第一次:
在这里插入图片描述

2020-04-19 15:55:35.564  INFO 20188 --- [nio-9900-exec-1] com.ymy.service.OrderService             : 前往数据库查询
2020-04-19 15:55:35.675  INFO 20188 --- [nio-9900-exec-1] com.ymy.service.OrderService             : 数据已经存入缓存

  
  
  • 1
  • 2

由于是第一次查询,所以缓存中不会存在数据,请求直接到达了数据库,并且获取到了id为1的数据,并且将数据添加到了缓存。
在这里插入图片描述

第二次查询id等于1的数据

2020-04-19 15:57:47.879  INFO 20188 --- [nio-9900-exec-5] com.ymy.service.OrderService             : 缓存中查询到了信息,直接返回:OrderBo(id=1, orderCode=202004191416, orderPrice=3299.00, peoductName=iphone se2, createTime=2020-04-19 14:17:07)

  
  
  • 1

我们发现他直接命中了缓存,直接返回,这是正常情况,那如果非正常情况呢?比如查询的订单id=-1呢?这个时候会发生什么事情?

http://localhost:9900/detail?id=-1
在这里插入图片描述
看到没有,请求全都进入数据库了,这种情况是肯定不被允许的,如果你的程序中存在这种情况,一定要赶紧修改,否则有可能会让一些心怀不轨的人直接将数据库的服务搞宕机,那这种问题如何解决呢?

解决方案

将空数据存入缓存

什么意思呢?简单点来说,不管数据库中有没有查询到数据,都往缓存中添加一条数据,这样下次请求的时候就会直接在缓存中返回,这种方式比较简单粗暴,我们一起看看如何实现。

代码改造:

OrderService.java

 public OrderBo getDetail(Long id) {
        //缓存中查询词词订单
        Object obj =  redisTemplate.opsForValue().get("order:" + id);
        if(obj != null ){
            String data = obj.toString();
            log.info("缓存中查询到了信息,直接返回:{}",data);
            return  "".equals(data)  ? null : (OrderBo) obj;
        }
        log.info("前往数据库查询");
        OrderBo orderBo =  orderMapper.getDetail(id);
        if(orderBo != null ){
            //将数据保存到数据库,有效时间一小时
           redisTemplate.opsForValue().set("order:" + id,orderBo,3600,TimeUnit.SECONDS);
           log.info("数据已经存入缓存");
        }else {
            redisTemplate.opsForValue().set("order:" + id,"",300,TimeUnit.SECONDS);
            log.info("数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中");
        }
        return orderBo;
    }

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

往缓存中添加数据的时候一定要注意值的问题,请看我这里,我添加的是一个空字符串,并不是null,是因为我判断的条件是缓存中!=null就直接返回,如果你往缓存中添加一条null的数据,这个时候就会和你的判断起冲突,又会进入到数据库了,所以这点需要特别注意,我们来看测试:

第一次请求:http://localhost:9900/detail?id=-1

2020-04-19 16:23:21.520  INFO 16596 --- [nio-9900-exec-6] com.ymy.service.OrderService             : 前往数据库查询
2020-04-19 16:23:21.577  INFO 16596 --- [nio-9900-exec-6] com.ymy.service.OrderService             : 数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中

  
  
  • 1
  • 2

第二次请求:http://localhost:9900/detail?id=-1

2020-04-19 16:24:25.855  INFO 16596 --- [nio-9900-exec-9] com.ymy.service.OrderService             : 缓存中查询到了信息,直接返回:

  
  
  • 1

这个时候请求命中了缓存,就不会前往数据库中了,但是这个需要注意一点:空值的过期时间不能设置的太长,什么意思呢?设想一下,我们现在数据库中只有id=1的数据,我们查询id=2也会往缓存中插入一条数据,但是这个时候数据库中新增了一条订单id=2,用户下次查询的时候看到你存储在缓存中中的数据,接直接回了空,但是数据库中明明已经添加了这条数据,这就是为什么过期时间不要设置太久的原因,当然了,我们也需要分情况考虑,比如查询id<=0的,我们都可以考虑永久存入缓存或者设置很长的过期时间,推荐设置很长的过期时间,为什么呢?因为订单id不存在会<=0,但是对于>=0,我们可以将过期时间设置为30秒等等,这个看业务需求即可。

布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

这个算法实现起来比上面第一种稍微复杂一点,这里就不具体说明了,如果感兴趣的话可以百度自行了解一下,不是很难。

缓存击穿

什么是缓存击穿

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db(数据库)

这个也不难理解,和缓存穿透有点像但是性质又不相同,都是缓存中没有数据,请求命中数据库,缓存穿透指的是数据库中不存在的数据,缓存击穿则是指缓存失效的问题。,这种情况不太好模拟,我们可以直接将缓存中数据清空,替代缓存数据过期。

代码还是上面的代码,不做任何修改,不过我们不再使用postman测试,而是采用jemter,首先我们删除缓存中的数据,模拟key已经过期,我们查询id=1的订单详细信息,但需要注意的是,我并不是发一个请求,而是100个同时请求,会发生什么呢?

线程数:
在这里插入图片描述
Http请求信息
在这里插入图片描述
聚合报告
在这里插入图片描述
我们发现100个并发请求全部成功,异常率为0,接下来就是重点了,控制台会打印什么呢?
在这里插入图片描述
这就是缓存击穿,是不是很恐怖,虽然命中数据库的次数不是很多,那是因为我们的并发请求不是很大,像双十一这种并发,如果存在这种问题,数据库可能撑不过3秒就炸了。

解决方案

自动更新

什么是自动更新呢?这个有点类似与jwt的自动刷新token机制,jwt的自动刷新token实现原理大致为:请求的时候判断一下token的剩余有效时间,如果有效时间小于设定的时间,那么jwt将生成一个新的token,然后再将次token重新设置过期时间,并将新的token返回给前端使用,这个也可以参考一下,redis是支持查询某个key剩余有效时间,所以这里我们只需要设定一个时间差,比如3分钟,请求的时候查询的有效时间如果小于3分钟,那么刷新这个key的有效时间,刷新这个操作可以使用异步实现(提高性能)。

可能你想到了,这种方式存在缺陷,没错,如果再快失效的3分钟内没有请求,那么缓存中的key将不会被刷新,还是会存在缓存击穿的问题,所以这种方式不是特别推荐。

定时刷新

定时刷新有两种方案

第一种:定时任务
查询快要过期的key,更新内容,并刷新有效时间,这种比较消耗服务器性能,也不是特别推荐。

第二种:延迟队列
如果大家了解它的话可能一下就知道我说的是什么意思了,将数据存入缓存的那一刻同时发送一个延迟队列(安指定时间消费),时间小于缓存中key的过期时间,到了指定时间,消费者刷新key的有效时间再发送一个延迟队列,以此循环,这种方式还是不错的,但是实现方式相对于第一种来说就要复杂一点了,他需要依靠消息中间件来完成,如果消息中间件某个时间宕机,那就gg了,虽然这种方式虽然比较推荐,但是成本偏高,因为为了防止消息中间件宕机,我们有可能需要对消息中间件做集群处理。

程序加锁

我个人推荐使用这个,为什么呢?因为它不需要额外的服务器开销,也不需要额外的资源消耗,他仅仅只是让线程串行而已,但是这个时候你可能就会有疑问了,加锁不是会严重影响程序的效率吗?为什么你还推荐这种方式呢?

其实并不是所有的锁都会很大的降低程序的性能,这里我们当然不能使用synchronized,原因很简单,他的效率比较慢,不太适合这种情况,我要介绍的这种锁名字为:读写锁

什么是读写锁?请参考我的另外一篇博客:【并发编程】java并发编程之ReentrantReadWriteLock读写锁

好了,我们一起来改造一下之前的代码

OrderService.java

package com.ymy.service;

import com.ymy.bo.OrderBo;
import com.ymy.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Service
@Slf4j
public class OrderService {

<span class="token keyword">private</span> RedisTemplate redisTemplate<span class="token punctuation">;</span>

<span class="token keyword">private</span> OrderMapper orderMapper<span class="token punctuation">;</span>

<span class="token keyword">private</span> <span class="token keyword">static</span>  <span class="token keyword">final</span> AtomicInteger count <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AtomicInteger</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> ReadWriteLock readWriteLock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantReadWriteLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token keyword">public</span> <span class="token function">OrderService</span><span class="token punctuation">(</span>RedisTemplate redisTemplate<span class="token punctuation">,</span> OrderMapper orderMapper<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>redisTemplate <span class="token operator">=</span> redisTemplate<span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>orderMapper <span class="token operator">=</span> orderMapper<span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token comment">/**
 * 通过id查询订单详情
 *
 * @param id
 * @return
 */</span>
<span class="token keyword">public</span>  OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span>Long id<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">int</span> num <span class="token operator">=</span> count<span class="token punctuation">.</span><span class="token function">incrementAndGet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">//获取读锁</span>
    Lock readLock <span class="token operator">=</span> readWriteLock<span class="token punctuation">.</span><span class="token function">readLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
        readLock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token comment">//缓存中查询订单信息</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往缓存中查询信息,第一次,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        Object obj <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>obj <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            String data <span class="token operator">=</span> obj<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"缓存中查询到了信息,直接返回:{}"</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token string">""</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">?</span> null <span class="token operator">:</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> obj<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"没有在缓存中获取到数据,即将前往数据库获取,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span>
        <span class="token comment">//释放读锁</span>
        readLock<span class="token punctuation">.</span><span class="token function">unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token comment">//获取写锁</span>
    Lock writeLock <span class="token operator">=</span> readWriteLock<span class="token punctuation">.</span><span class="token function">writeLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span><span class="token punctuation">{</span>
        writeLock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token comment">//缓存中查询订单信息</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"第二次前往缓存中查询信息,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        Object obj <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>obj <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            String data <span class="token operator">=</span> obj<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"缓存中查询到了信息,直接返回:{}"</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token string">""</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">?</span> null <span class="token operator">:</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> obj<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往数据库查询,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        OrderBo orderBo <span class="token operator">=</span> orderMapper<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"数据库返回的数据:{},这是第:{}次请求"</span><span class="token punctuation">,</span>orderBo<span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token comment">//将数据保存到数据库,有效时间一小时</span>
            redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span> orderBo<span class="token punctuation">,</span> <span class="token number">3600</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>
            log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"数据已经存入缓存,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
            redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>
            log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中,这是第:{}次请求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">return</span> orderBo<span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token keyword">finally</span> <span class="token punctuation">{</span>
        writeLock<span class="token punctuation">.</span><span class="token function">unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

加了读写锁之后我们一起来看看控制台的输出结果:

在这里插入图片描述
这只是其中一部分,由于输出的内容过长我就不全部展示出来了,我们这里需要关注的只有一个,数据库查询了多少次?

我们将控制台日志拷贝到notepad++中,搜索“数据库返回的数据”,请看结果:
在这里插入图片描述我们可以看到,查询数据库的操作只有一处,但是查询缓存的确实并发执行的,这就是为什么我推荐使用读写锁的原因,读写锁中读锁和写锁是互斥的,你觉得这样速度还是不够快,能不能读锁和写锁并行?答案是肯定的,请参考我的另外一篇博客:【并发编程】面试官:有没有比读写锁更快的锁?

缓存雪崩

什么是缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机

为什么会出现大批量的key过期呢?是不是我们设置了相同的过期时间导致的?

解决方案

随机设置过期时间

这个随机时间并不是真正的随机时间,而是指在原来过期时间的基础上生成一个随机时间,这个随机时间比较小,然后两者相加即可。

设置永久有效

将一些常用的数据设置成为永久有效,注意哦,是经常使用的而不是全部,这点需要特别注意。

总结

什么是缓存穿透?
同一时刻,大量的并发请求数据库中不存在的信息,他既不会命中缓存,也不会命中数据库,但是他会查找数据库

什么是缓存击穿?
缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db(数据库)

什么是缓存雪崩?
缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机

并不是只有上面几种解决方案,这里我只是讲解了几种常用的解决方案,在日常开发中我们可以根据实际的业务需求进行选择,没有最好的,只有最适合自己的,所以不一定要选择最牛逼的解决方案,但是一定要选择最适合项目的解决方案。
在这里插入图片描述

发布了753 篇原创文章 · 获赞 605 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/weixin_43336281/article/details/105698832