当我们使用一项技术时,我们就需要对它有一定的了解,知道我们为什么要去使用它,能够分析使用这项技术所带来的的回报以及我们所需要付出的代价。
缓存所带来的收益:
** 高速读写:缓存会加速读写速度,利用CPU L1/L2/L3 Cache、Linux page Cache加速硬盘读写、浏览器缓存、Ehcache缓存缓存数据,其性能都会比关系型数据库高很多,内存级别的读写性能大大优于磁盘级别的读写性能。**
** 降低后端负载:后端服务器业务通过使用Redis减少对MySQL的请求访问,降低MySQL负载等。**
缓存所带来的代价:
** 数据不一致:缓存层和数据库层面数据不一致,例如Redis已经对某条复杂的数据进行缓存,此时通过后台修改该数据,由于大部分情况下我们不会主动对Redis进行数据删除,因为从性能以及包容性来说都不需要进行删除操作,所以就导致缓存数据同步不及时,这些都和我们自身应用更新策略有关,需要根据实际情况合理设置数据缓存过期时间等相关操作。**
代码维护成本(人工成本):不适用Redis缓存的情况下,我们只需读写MySQL就能实现功能,但当我们加入缓存之后就需要去维护缓存数据的处理逻辑,增加了代码复杂度。某些情况下会降低项目开发以及测试效率,例如测试人员需要测试有缓存时的情况,也要测试没有缓存时的情况,另外当测试人员测试功能无需关注缓存时需手动清除对应缓存,否则需要等待数据缓存过期,延长测试时间。
性能风险:堆内缓存由于是存储在本地服务器中,由JVM或者本地服务器来维护数据,可能带来内存溢出的风险甚至影响用户进程,如ehCache、loadingCache。
堆内缓存和远程服务器缓存如何选择?对于这个问题主要考虑以下几点:
堆内缓存一般性能更好,远程缓存需要套接字传输。
** 用户级别缓存尽量采用远程缓存(例如集群架构下,多台机器会重复缓存同一组数据)。**
** 大数据量尽量采用远程缓存,避免造成应用服务器压力过大,遵循服务节点化原则。**
[2.缓存穿透]
** 在我们使用Redis的过程中,Redis的确帮助我们解决了很多的问题,但是当技术和业务结合在一起时就会发现一些让人深思的问题。缓存穿透就是其中一个。**
[2.1 什么是缓存穿透?]
上面这张图是我们正常业务场景下的一个流程(客户端->服务端[Redis->DB]),那么缓存穿透指的是当用户查询数据,再缓存中不存在,并且在数据库也不存在时,导致用户查询这样的数据(或者恶意攻击) 在缓存中找不到对应key的value,每次都需要要在数据库中查询一遍,然后返回空值。其实这就相当于进行了两次无用的查询,这样请求就绕过缓存直接查数据库,对数据库造成了很大的压力。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
[2.2 如何防止缓存穿透?]
说到如何防止缓存穿透,这里我主要提出两点建议:
缓存空值:如果DB查询返回数据或者业务结果为空,此时我们仍然将空结果进行缓存,设置较短的过期时间(不超过五分钟)。
** 采用布隆过滤器BloomFilter:事先将所有可能存在的数据哈希后放入到一个足够大的BitMap中,若一个数据一定不存在则会被拦截掉,从而避免了对底层存储系统的查询压力。关于布隆过滤器后面会写一篇博客对其进行详细介绍。**
[3.缓存雪崩]
[3.1 什么是缓存雪崩?]
** 如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有查询都落在数据库上,就会造成了缓存雪崩。 由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都需要去查询数据库,从而对数据库服务CPU和内存造成巨大压力,严重可能会造成数据库服务宕机。**
[3.2 如何防止缓存雪崩?]
这里我们看看如何防止缓存雪崩:
** 加锁排队:维护一个mutex互斥锁,通过Redis的SETNX命令去设置一把当前业务操作的锁(例如setnx lock_uid_001 value nullValue、setnx lock_white_list value nullValue),当操作成功时,再进行LoadDB操作回设缓存,否则重试get缓存。**
数据预热:缓存预热就是当我们新部署一个项目系统时,将相关缓存数据直接加载到缓存中。这样可以避免在用户请求时候,再去查询数据库放入缓存。用户可以直接查询事先被预热的缓存数据,可以预热大并发量、热度高的Key缓存数据。
** 双层缓存策略:同一条数据我们可以维护两套缓存,C1为原始缓存,C2为拷贝缓存,当C1失效时,可以读取C2,都不存在时再去访问数据库回设,当然C1缓存失效时间需要设置为短期,而C2设置为长期。这种方式最大缺点就是占用的内存翻倍。**
** 定时更新缓存策略:对于失效性要求低的缓存,可以在容器启动初始化时加载放入缓存,采用定时任务更新或移除缓存。**
** 时间浮动策略:设置不同过期时间,让缓存失效的时间点尽量均匀,避免集中失效**
[穿透]
** 穿透:频繁查询一个不存在的数据,由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。**
解决办法:
** 1.用一个bitmap和n个hash函数做布隆过滤器过滤没有在缓存的键。**
** 解释:将数据库的id根据n个hash函数存储到对应的bitmap二进制数组里面,然后通过修改为1的标志来进而判断位置是否相同来确定是否存在对应的key在此redis里面,从而达到了减少扫描redis内存数据的方案。(确定无法一定不存在的key)。**
** 2.持久层查询不到就缓存空结果,有效时间为数分钟。(存储空结果),防止大量查询DB**
雪崩
** 雪崩:缓存大量失效的时候,引发大量查询数据库。**
解决办法:
** 1.用锁/分布式锁或者队列串行访问**
** 2.缓存失效时间均匀分布**
** 3.缓存预热,在启动或者初始化 加载一些缓存数据进入,防止大量去查询访问DB**
** 4.二级双层缓存机制,当一级失效则去访问二级远程的MemoryCache机制。**
[热点key]
** 热点key:某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。**
解决办法:
① 使用锁,单机用synchronized,lock等,分布式用分布式锁。
② 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。
③ 在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。