1.缓存
缓存,数据交换的缓冲区,针对缓冲对象的不同(不同的硬件)都可以构建缓存。
目的是,把读写速度慢的介质数据保存在读写速度快的介质中,从而提高读写的速度,减少时间消耗。例如:
- CPU高速缓存:高速缓存的读写速度远高于内存。
- CPU读数据时,如在高速缓存中找到所需数据,就不需要读内存
- CPU写数据是,先写入到高速缓存中,在回写到内存
- 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存的读写数据也远高于磁盘的
- 读数据时从内存中去取
- 写数据时,可以先写到内存中,定时或者定量回写到磁盘,或者同步回写。
目的:使用缓存的目的就是为了提升读写性能。在实际的业务场景中就是为了提升读性能,带来更好的性能,更高的并发量
2.缓存算法
- LRU(least recentlty used,最近最少使用)算法
- LFU(least Frequently used,最不经常使用)
- FIFO(first in first out,先进先出)
3.缓存穿透
存储穿透:是指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从DB查不到数据就不写入缓存,这将导致这个不存在的数据一直去请求DB,是去缓存的意义。
被动写:当从缓存中查询不到数据的时候,从DB查到数据,然后将数据写入到缓存。
如何解决?
-
方案一,缓存空对象。
当从DB查询数据为空的时候,我们仍然将这个结果进行缓存,具体的值需要使用特殊的标识,能和真正的缓存数据区分开。另外需要设置较短的过期时间,不建议超过5分钟。缓存时间太久没意义,浪费缓存内存。
-
方案二,BloomFilter布隆过滤器
在缓存服务的基础上,构建BloomFilter数据结构。在BloomFilter中存储对应的key是否存在,如果存在,再说明该key的值不为空,逻辑如下:
- 根据key查询BloomFilter,如果不存在对应的值,直接返回。如果存在,继续向下执行。
- 根据key的值,查询缓存的值,如果存在,直接返回,不存在,向下执行。
- 查询db的值,如果存在,更新到缓存,直接返回。
为什么BloomFilter不存储Key是不存在的情况?
- BloomFilter存在误判。简单来说,存在的不一定存在,不存在的不一定不存在。一个误判的key就会被误判为不存在。
- BloomFilter不允许删除。如果一个key开始不存在,后面又有数据了,这时候会被判断为一致不存在。
缓存空对象 BloomFilter 使用场景 1.数据命中率不高;2.保证一致性 1.数据命中不高。2数据相对固定,实时性低 维护成本 1.代码维护简单;2.需要过多的缓存空间;3.数据不一致 1.代码维护复杂;2.缓存空间占用少
4.缓存雪崩
概念
缓存雪崩,是指缓存由于某些原因无法提供服务(比如缓存挂了),所有的请求到达DB,导致DB负荷增大,最终挂掉的情况。
如何解决
-
缓存高可用
通过搭建缓存的高可用,避免缓存挂掉导致无法提供服务的情况,从而降低出现缓存雪崩的情况。Redis可以通过搭建Redis Sentinel或者Redis Cluster来做缓存高可用
-
本地缓存
使用本地缓存,即使分布式缓存挂了,也可以将DB查询到的数据缓存到本地,避免后续请求全部到达DB。
-
请求DB限流
通过限制DB的每秒请求数,避免DB挂掉,这样做的好处:
- 有一部分用户任然可以使用该系统。
- 缓存服务恢复后,立即可以使用。不用再去管理DB服务
被限流的请求,我们可以通过服务降级,提供一些默认的值,比如空白页面、友情提示等到。我们可以通过Sentinel、Hystrix等来实现。
引入本地缓存的问题
- 本地缓存实时性如何保证
- 引入消息队列。在数据更新时,发布数据更新的消息;而进程中有相应的消费者消费该消息,从而更新本地缓存;
- 设置较短的过期时长,请求从DB拉取数据;
- 通过手动过期;
- 每个进程可能会在本地缓存相同的数据,导致资源浪费?
- 需要配置本地的缓存过期策略和缓存数量上限。
5.缓存击穿
概念
缓存击穿,是指在某个热点数据在某个时间点过期的时候,恰好这个时间对这个key有大量的的并发请求过来,这些请求发现缓存过期,都会去请求到DB加载数据并回写到缓存,这个时候过大的并发就有可能瞬间把DB压垮。
如何解决
-
方案一,使用互斥锁
请求发现缓存不存在后,去查询DB前,使用分布式锁,保证只有一个线程拿到锁,然后取请求DB,然后更新写入缓存
- 1获取分布式锁,直到成功或者超时。如果超时则抛出异常,成功,继续向下执行
- 2、获取缓存,如果存在值,直接返回,如果不存在,继续。
- 3、查询DB,更新到缓存,返回值。
-
方案二,手动过期
缓存上不设置过期时间,功能上将过期时间设置在Value中。流程如下
- 1、获取缓存,通过value中的时间来比较是否过期。如果未过期,直接返回,如果过期,继续向下执行。
- 2、通过一个后台的异步线程进行缓存的构建,也就是“手动过期”。通过后台的异步线程,保证只有一个线程查询DB。
- 3、同时,虽然Value还是过期,但还是直接返回。通过这样的方式保证了服务的可用性,但是损失了一定的实效性。
6.缓存与DB一致性保持
产生不一致的原因
-
并发场景下,导致老的DB数据写入到缓存中。
这里指的是,更新DB之前,先删除Cache中的数据。在低并发的情况下,不会出问题,但是在高并发的情况下就会出问题。在删除Cache和更新DB之间,这里恰好有请求进来,这时候使用被动读,因为在DB中的数据还是老数据,这时候又会将老数据写入到缓存中。
缓存和DB的操作不在同一个事物中,可能DB操作成功,二cache操作失败,这样会导致不一致
解决方案
-
将缓存可能出现的并行写,实现串行写
这里指的缓存并行写。在被动读中,如果缓存不存在,也存在写。
-
在写请求前,先淘汰缓存之前,先获取分布式锁。
写-->获取锁-->delete cache-->update db ---> write cache
-
在读请求时,如果缓存不存在,先去获取分布式锁
读-->读cache,如果null-->获取锁-->查询cache,如果null-->查db-->write cache
-
-
实现缓存的最终一致性。
-
先淘汰缓存,在写数据库。
因为先淘汰缓存,所以数据的最终一致性是可以保证的。为什么尼?先淘汰缓存,即使写数据库发生异常,在下次缓存读取时候,多读取一次数据库。
-
先写数据库,在更新缓存
需要保证写数据库和更新缓存的操作,能够在一个”事务“中,从而实现最终一致性。
基于定时任务来实现
- 首先,写入数据库
- 然后在写入数据库所在的事物中,插入一个记录的任务表,该记录表存储需要存入cache中的key和value
- 【异步】遍历任务表,写入cache
基于消息队列来实现
- 首先,写入数据库
- 然后发送带我缓存key和value的事物消息。
- 【异步】消费者消费该消息,写入缓存
基于数据库的binlog日志
image- 应用直接将数据写入数据库
- 数据库会更新binlog日志
- 利用Cannal中间件读取binlog日志
- Cannal借助于限流主键按频率将数据发送到MQ中
- 应用监控MQ渠道,将MQ的数据缓存到cache中
-
7.缓存预热
启动时,先将热点数据缓存到缓存中
如何实现
- 数据量不大的时候,项目启动,自动进行初始化
- 通过修复脚本,执行脚本
- 写管理页面,手动操作
7.缓存淘汰策略
- 定时清理
- 当用户请求过来的时候,去判断是否过期,过期的话就去底层系统获取数据,进行更新