当我们使用一项技术时,我们就需要对它有一定的了解,知道我们为什么要去使用它,能够分析使用这项技术所带来的的回报以及我们所需要付出的代价。
缓存所带来的收益:
高速读写:缓存会加速读写速度,利用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,每次都需要要在数据库中查询一遍,然后返回空值。其实这就相当于进行了两次无用的查询,这样请求就绕过缓存直接查数据库,对数据库造成了很大的压力。
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并做更新缓存操作。