Redis因为其自身高性能的数据读取能力,因此会经常被应用到缓存的场景中,本文就一起看下Redis当做缓存使用时的特点,问题,以及需要注意的点。
从架构模式上来看缓存系统可以分为旁路缓存,内嵌缓存,分别来看下。
旁路缓存在系统架构中所处的位置如下图:
数据读取方式如下:
1:当缓存中有数据时直接读取缓存数据
2:当缓存中没有数据时,从后端存储中读取数据,并将数据写入到缓存中
数据写入方式如下:
1:同时写入缓存和后端存储,保持数据的强一致性
2:数据仅写入后端存储,并是缓存失效
这种方式由应用程序负责管理缓存,即需要在应用程序中写缓存管理相关代码,可能代码如下:
本文我们要分析的Redis作为缓存使用就是旁路缓存。
内嵌缓存在系统架构中所处的位置如下图:
数据读取方式如下:
1:当缓存中有数据时,则直接返回数据
2:当缓存中没有数据时,直接从后端存储中读取数据(透读),缓存数据,并返回数据
数据写入方式:
1:数据写入缓存后,缓存以同步(透写)的方式将数据写入到后端存储
2:数据写入缓存后,缓存以异步(后写)的方式将数据写入到后端存储
我们的计算机系统使用的缓存就是内嵌缓存,因为CPU,内存,磁盘的数据存取速度差别很大,所以需要采用缓存机制,三者的时间级别如下图:
为了避免因为获取下层数据拖慢整个过程,CPU提供了LLC内嵌缓存,用来加速从内存中获取数据,内存提供了page cache用来加速从磁盘中获取数据,此时结构如下图:
缓存系统主要有以下两个特征:
1:缓存是一个快速子系统,能够提供快速的数据存取服务
2:缓存系统的容量大小一定小于后端慢速系统,即不可能将慢速系统所有数据放到快速系统中
对于以上两个特点,1Redis是满足的,2因为Redis提供了数据淘汰策略,所以也是满足的,所以Redis是适合作为缓存系统来使用的。
只读缓存,读写缓存。
仅仅提供数据读取,如果是数据写入的话,则直接写到后端存储中,缓存中的数据直接失效,下次从后端存储查询后再写到缓存中,如下图:
读写缓存是,缓存也接收更新,有如下两种方式:
同步直写:即更新缓存后同时更新后端存储
异步写回:即只更新缓存,后续再更新后端存储
这两种方式如下图:
同步直写会降低程序的性能,但是保证数据不丢失和一致性,异步写回,可能会导致数据丢失,但是可以提高更新速度,从而提高程序性能。
缓存满了,到底是缓存真的满了,还说说是我们设置的内存太小了!所以在看缓存满了怎么办
这个问题之前,首先要来看下如何设置一个合适的缓存大小。
设置容量我们可以参考八二原理
,即百分之八十的访问量是由百分之二十的数据贡献的,如下图:
图中展示的蓝色区域就是20%数据贡献的80%访问量,而粉色区域就是剩余的80%的数据贡献的20的访问量,即蓝色区域面积:粉红色区域面积=8:2
,当然这只是统计学意义上的一个概念,和我们实际的业务场景还是有一定偏差的,所以结合不同的业务场景,这个20%可以是一个范围,这个范围是15%到30%
,兼顾访问性能和内存带来的开销,那么具体应该怎么做呢?可以这样,比如一共有10G的数据,我们可以先设置为20%,如2G,然后观察其缓存命中率 ,如果命中率特别高,比如达到了95%,则我们就可以适当的降低内存,但是如果缓存命中率不很理想,比如70%,则我们就需要适当的调高内存,一般能达到90%左右就是一个比较理想的状态了。
比如确定了内存为4g则可以通过命令config set maxmemory 4gb
来动态调整。那么,缓存大小已经确定了,当缓存满了该怎么办呢?这就需要看下缓存的淘汰策略了。
redis当前一共提供了8中淘汰策略,通过配置项maxmemory-policy
来配置,默认是noeviction
,其含义是不淘汰,这种策略当缓存满时如果写入数据,会因为无法写入数据而直接返回错误,这里的noeviction也正是redis提供的8中淘汰策略的一种。对于这8中淘汰策略,我们可以按照是否淘汰数据来划分,对于淘汰数据又可以按照从设置了过期时间的数据中淘汰,和从所有的数据中淘汰,如下:
不淘汰数据:noeviction
淘汰数据:
从设置了过期时间的数据中淘汰
volatile-random
volatile-lru
volatile-lfu
volatile-ttl
从所有的数据中淘汰
allkeys-random
allkeys-lru
allkeys-lft
可以看到从设置了过期时间的数据中淘汰
的淘汰策略相比于从所有的数据中淘汰
仅仅多了一个基于过期时间的ttl策略。接下来我们分别看下每种淘汰策略具体的工作方式。
不淘汰数据,当空间不足无法写入新数据时,停止对外服务,直接给写入请求返回错误。
从设置了过期时间的数据中随机删除。
从设置了过期时间的数据中通过lru算法删除,这里介绍下lru(least recent used)算法,该算法通过一个队列维护数据,队列头叫做MRU端(most recent used),对列尾叫LRU(least recent used)端,当某个数据被访问或新写入时,就会被移动到MRU,其他数据后移,如下图:
但是实际应用时,因为Redis的数据较多,如果对全部数据维护一个大的lru链表的话,会带来额外的空间开销,因此,Redis对该算法进行了优化,具体是首先在RedisObject的元数据中维护lru属性作为数据最近被访问的时间,当需要淘汰数据时,随机的选择的N(通过参数max-samples配置)
个数据,然后选择其中lru值最小的进行删除。
类似于lru,但是在RedisObject的元数据中又多维护了一个访问次数的信息counter,当淘汰数据时也是随机选择N个数,但是淘汰数据的策略是,优先淘汰访问次数最少的,如果是访问次数相同的话,则淘汰最近一次访问时间最早的(这和lru是一致)
。
从设置了过期时间的数据中选择一个最接近过期时间的删除。
从所有数据中随机选择一个删除。
对所有数据使用lru算法删除,同4.2.3:volatile-lru
。
同4.2.4:volatile-lfu
,不同之处是从所有的数据中选择。
allkeys-lru:业务数据有明显的冷热数据区分,让最常访问的数据保留下来,提升访问性能。
allkeys-random:业务数据没有明显的冷热数据区分,访问比较均匀
volatle-lru:存在所有用户一定会查看的数据,比如全站的通知,置顶的新闻等,对于这些数据可以不设置过期时间,这样永远不会被删除,其他数据设置过期时间通过lru淘汰
在分析这个问题之前,我们要先看下这里的一致性是什么,主要以下两方面:
1:缓存和数据库中都有数据,二者数据必须一致,且都是最新数据
2:缓存中没有数据时,数据库中的数据必须是最新数据
以上两种的任何一种情况发生我们就说出现了数据不一致。我们知道,Redis一般作为旁路缓存使用,而具体的缓存类型又可以分为只读缓存和读写缓存,关于数据一致性的问题,我们也需要从这两种缓存类型切入进行分析。
我们先来回顾下读写缓存的特点,读写缓存即更新时先更新缓存(目的是提高更新速度)
,对于后端存储的更新有以下的两种方式:
同步直写策略:写缓存时同步更新后端存储
异步写回策略:写缓存后,异步的写后端存储
其中同步写回策略不会有数据一致性的问题,但是需要引入事务,保证二者操作的原子性。对于异步写回策略,如果是异步更新后端存储失败,则会出现数据一致性问题。所以,对于读写缓存要想解决数据一致性问题,可以采用同步直写策略
,或者是增加重试机制。
注意以下分析,基于业务接受短时间内读取到不一致的数据,对于业务不接受任何数据不一致的场景,就需要考虑锁等待。
对于只读缓存我们需要将操作分成两类来进行分析,即新增数据,和删改数据。
新增数据时,会直接将数据写到后端存储,此时缓存中是没有数据的,符合缓存中没有数据时,数据库中的数据必须是最新数据
的条件,因此此时不存在数据一致性问题。
对于删改数据,我们可以分为以下不同的场景来分析。
那么此时,怎么办呢?出现问题的根本原因是后更新数据库失败,因此我们只需要加入重试机制再次更新数据库就行了。
出现数据不一致的原因是红框中的操作,此时,我们只需要在一段时间后删除红框中的写入就可以了,即线程A操作完成后sleep一段时间,再一次删除缓存,这个sleep的时长可以通过计算完成数据库读取+写入到缓存
的时间估算出来。此时伪代码可能如下:
这3个问题都是因为无法通过缓存获取数据,进而导致请求大量涌入数据库系统,造成数据库的巨大压力,甚至导致数据库的宕机,进而引发线上事故。分别看下。
缓存雪崩的定义是大量数据无法在缓存中获取,进而导致应用层将请求发送到数据库,给数据库造成巨大的压力。一般缓存雪崩由两方面原因造成,首先是大量的key在同一时间过期,其次是redis实例宕机,针对这两种情况来分别看下如何处理。
这种情况参考下图:
针对大量key同时过期造成的缓存雪崩,我们可以从事前预防和事后补救两方面来进行分析。
1~3分钟
。这种事前预防是业务无损的,实际应用中可以放心的使用。
注意:这种方式是业务有损的,实际应用还是要格外小心!
从事前预防和事后补救两方面来分析。
服务限流是,只允许一定比例的请求执行,剩余的请求直接返回,如下图:
缓存击穿是一些热点数据因为过期被删除导致请求挤压到数据库,如下图:
这种情况解决方案是,对于会频繁访问的热点数据不要设置过期时间。
缓存穿透不同于缓存雪崩和缓存击穿,后二者都是缓存中没有数据,但是在数据库中有数据,缓存穿透是在缓存中和数据库中都没有数据,但是效果是一样的,都是请求全部到数据库,给数据库造成巨大的压力,但因为数据库中没有数据,所以其影响要比缓存雪崩和缓存击穿更严重,参考下图:
造成缓存穿透的原因可能如下:
1:程序错误,导致同时删除了缓存和数据库中的数据
2:恶意攻击,故意访问不存在的数据
对于缓存穿透有如下几种方案。
对于缓存中没有数据库中也没有的值,我们可以和业务方来约定一个值代表这种情况,比如notExitsInCacheAndDb
,当返回该值时业务方就知道发生了缓存穿透,可以做特殊处理,并且该约定值也会被缓存到缓存中,下次可以使用。
直接查询数据库判断是否存在的成本较高,会增加数据库的压力,此时,这个判断过程可以通过布隆过滤器完成,布隆过滤器的工作原理是,使用若干个bit的0和1来表示该值是否存在,而具体是哪几个位则通过对应个数的哈希函数获取,如hash(key)%bit数
就是对应的bit的位置,当这若干个bit位有一个为0时,则说明肯定不存在,工作原理可以参考下图:
因此,我们只需要将数据库中的值,维护到这个bit数组中,后续判断时直接通过布隆过滤器来判断,如果是不存在就不用通过数据库来查询了,减轻了数据库的压力。
程序中可以通过参数等信息来判断是否为恶意请求,如果是恶意请求则直接过滤,这种方式和业务关系密切,需要好好考虑下如何做,不然可能出现误杀的情况。
访问次数很少的数据,一直保留在缓存中,占用缓存空间的情况就是缓存污染。即不会或几乎不会再次被访问的数据一只保留在缓存中。
缓存污染是不应该继续保留在缓存中数据留在缓存中,因此我们需要通过一定的手段来将其淘汰出缓存,而淘汰缓存就需要用到缓存的淘汰策略,因此这里一定的手段就是配置合适缓存淘汰策略了,接下来我们就针对每种缓存淘汰策略看下其是否适合用来解决缓存污染问题。
该策略在缓存满时不会淘汰数据,因此不能用来解决缓存污染问题。
随机删除,可能删除经常访问的数据,当然也可能删除造成缓存污染的数据,但是随机性太强,也不能用来解决缓存污染问题,并且是只会针对设置了过期时间的数据,如果是没有设置过期时间的话,则无法处理。
如果是都设置了过期时间的话,适合用来解决缓存污染的问题。但是有一种情况不适合,即存在大批量的key单次扫描的情况,此时大量的key的访问时间都很新,也就无法淘汰,但是访问次数都很少,这种场景就需要考虑lfu算法的淘汰策略。
如果是都设置了过期时间的话,适合用来解决缓存污染问题。
正常数据的过期时间并不能反映出其是否是造成缓存污染的数据,所以不适合用来解决缓存污染问题,但是如果是业务上清晰的知道哪些数据使用的时间长,哪些数据使用的时间短,并基于此设置了对应有效期的话,则适合用来解决缓存污染的问题。
类似于volatile-lru,但也无法解决大批量key单次扫描的问题,也需要使用lfu。
适合用来解决缓存污染问题。
随机删除数据,作用有限,不能用来解决缓存污染问题。
参考文章列表:
缓存的几种架构模式 。