缓存详解

时间:2021-7-5 作者:qvyue

1.缓存

缓存,数据交换的缓冲区,针对缓冲对象的不同(不同的硬件)都可以构建缓存。

目的是,把读写速度慢的介质数据保存在读写速度快的介质中,从而提高读写的速度,减少时间消耗。例如:

  • CPU高速缓存:高速缓存的读写速度远高于内存。
    1. CPU读数据时,如在高速缓存中找到所需数据,就不需要读内存
    2. CPU写数据是,先写入到高速缓存中,在回写到内存
  • 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存的读写数据也远高于磁盘的
    1. 读数据时从内存中去取
    2. 写数据时,可以先写到内存中,定时或者定量回写到磁盘,或者同步回写。

目的:使用缓存的目的就是为了提升读写性能。在实际的业务场景中就是为了提升读性能,带来更好的性能,更高的并发量

2.缓存算法

  1. LRU(least recentlty used,最近最少使用)算法
  2. LFU(least Frequently used,最不经常使用)
  3. FIFO(first in first out,先进先出)

3.缓存穿透

存储穿透:是指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从DB查不到数据就不写入缓存,这将导致这个不存在的数据一直去请求DB,是去缓存的意义。

被动写:当从缓存中查询不到数据的时候,从DB查到数据,然后将数据写入到缓存。

缓存详解
image

如何解决?

  1. 方案一,缓存空对象。

    当从DB查询数据为空的时候,我们仍然将这个结果进行缓存,具体的值需要使用特殊的标识,能和真正的缓存数据区分开。另外需要设置较短的过期时间,不建议超过5分钟。缓存时间太久没意义,浪费缓存内存。

  2. 方案二,BloomFilter布隆过滤器

    在缓存服务的基础上,构建BloomFilter数据结构。在BloomFilter中存储对应的key是否存在,如果存在,再说明该key的值不为空,逻辑如下:

    1. 根据key查询BloomFilter,如果不存在对应的值,直接返回。如果存在,继续向下执行。
    2. 根据key的值,查询缓存的值,如果存在,直接返回,不存在,向下执行。
    3. 查询db的值,如果存在,更新到缓存,直接返回。

    为什么BloomFilter不存储Key是不存在的情况?

    1. BloomFilter存在误判。简单来说,存在的不一定存在,不存在的不一定不存在。一个误判的key就会被误判为不存在。
    2. BloomFilter不允许删除。如果一个key开始不存在,后面又有数据了,这时候会被判断为一致不存在。
    缓存空对象 BloomFilter
    使用场景 1.数据命中率不高;2.保证一致性 1.数据命中不高。2数据相对固定,实时性低
    维护成本 1.代码维护简单;2.需要过多的缓存空间;3.数据不一致 1.代码维护复杂;2.缓存空间占用少

4.缓存雪崩

概念

缓存雪崩,是指缓存由于某些原因无法提供服务(比如缓存挂了),所有的请求到达DB,导致DB负荷增大,最终挂掉的情况。

如何解决

  1. 缓存高可用

    通过搭建缓存的高可用,避免缓存挂掉导致无法提供服务的情况,从而降低出现缓存雪崩的情况。Redis可以通过搭建Redis Sentinel或者Redis Cluster来做缓存高可用

  2. 本地缓存

    使用本地缓存,即使分布式缓存挂了,也可以将DB查询到的数据缓存到本地,避免后续请求全部到达DB。

  3. 请求DB限流

    通过限制DB的每秒请求数,避免DB挂掉,这样做的好处:

    • 有一部分用户任然可以使用该系统。
    • 缓存服务恢复后,立即可以使用。不用再去管理DB服务

    被限流的请求,我们可以通过服务降级,提供一些默认的值,比如空白页面、友情提示等到。我们可以通过Sentinel、Hystrix等来实现。

引入本地缓存的问题

  • 本地缓存实时性如何保证
    • 引入消息队列。在数据更新时,发布数据更新的消息;而进程中有相应的消费者消费该消息,从而更新本地缓存;
    • 设置较短的过期时长,请求从DB拉取数据;
    • 通过手动过期
  • 每个进程可能会在本地缓存相同的数据,导致资源浪费?
    • 需要配置本地的缓存过期策略和缓存数量上限。

5.缓存击穿

概念

缓存击穿,是指在某个热点数据在某个时间点过期的时候,恰好这个时间对这个key有大量的的并发请求过来,这些请求发现缓存过期,都会去请求到DB加载数据并回写到缓存,这个时候过大的并发就有可能瞬间把DB压垮。

如何解决

  1. 方案一,使用互斥锁

    请求发现缓存不存在后,去查询DB前,使用分布式锁,保证只有一个线程拿到锁,然后取请求DB,然后更新写入缓存

    • 1获取分布式锁,直到成功或者超时。如果超时则抛出异常,成功,继续向下执行
    • 2、获取缓存,如果存在值,直接返回,如果不存在,继续。
    • 3、查询DB,更新到缓存,返回值。
  2. 方案二,手动过期

    缓存上不设置过期时间,功能上将过期时间设置在Value中。流程如下

    • 1、获取缓存,通过value中的时间来比较是否过期。如果未过期,直接返回,如果过期,继续向下执行。
    • 2、通过一个后台的异步线程进行缓存的构建,也就是“手动过期”。通过后台的异步线程,保证只有一个线程查询DB。
    • 3、同时,虽然Value还是过期,但还是直接返回。通过这样的方式保证了服务的可用性,但是损失了一定的实效性。

6.缓存与DB一致性保持

产生不一致的原因

  1. 并发场景下,导致老的DB数据写入到缓存中。

    这里指的是,更新DB之前,先删除Cache中的数据。在低并发的情况下,不会出问题,但是在高并发的情况下就会出问题。在删除Cache和更新DB之间,这里恰好有请求进来,这时候使用被动读,因为在DB中的数据还是老数据,这时候又会将老数据写入到缓存中。

  2. 缓存和DB的操作不在同一个事物中,可能DB操作成功,二cache操作失败,这样会导致不一致

解决方案

  1. 将缓存可能出现的并行写,实现串行写

    这里指的缓存并行写。在被动读中,如果缓存不存在,也存在写。

    1. 在写请求前,先淘汰缓存之前,先获取分布式锁。

      写–>获取锁–>delete cache–>update db —> write cache

    2. 在读请求时,如果缓存不存在,先去获取分布式锁

      读–>读cache,如果null–>获取锁–>查询cache,如果null–>查db–>write cache

  2. 实现缓存的最终一致性。

    1. 先淘汰缓存,在写数据库。

      因为先淘汰缓存,所以数据的最终一致性是可以保证的。为什么尼?先淘汰缓存,即使写数据库发生异常,在下次缓存读取时候,多读取一次数据库。

    2. 先写数据库,在更新缓存

      需要保证写数据库和更新缓存的操作,能够在一个”事务“中,从而实现最终一致性。

      基于定时任务来实现

      • 首先,写入数据库
      • 然后在写入数据库所在的事物中,插入一个记录的任务表,该记录表存储需要存入cache中的key和value
      • 【异步】遍历任务表,写入cache

      基于消息队列来实现

      • 首先,写入数据库
      • 然后发送带我缓存key和value的事物消息。
      • 【异步】消费者消费该消息,写入缓存

      基于数据库的binlog日志

      缓存详解
      image
      • 应用直接将数据写入数据库
      • 数据库会更新binlog日志
      • 利用Cannal中间件读取binlog日志
      • Cannal借助于限流主键按频率将数据发送到MQ中
      • 应用监控MQ渠道,将MQ的数据缓存到cache中

7.缓存预热

启动时,先将热点数据缓存到缓存中

如何实现

  1. 数据量不大的时候,项目启动,自动进行初始化
  2. 通过修复脚本,执行脚本
  3. 写管理页面,手动操作

7.缓存淘汰策略

  1. 定时清理
  2. 当用户请求过来的时候,去判断是否过期,过期的话就去底层系统获取数据,进行更新

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。