关于缓存使用和注意事项的思考

        发明BSD、TCP/IP、csh、vi和NFS的SUN首席科学家Bill Joy说过,在计算机体系结构领域里,缓存是唯一称得上伟大的思想的。其他的一切发明和技术不过是在不同场景下应用这一思想而已。在计算机软件领域里,情形也大体相似。如果罗列这个领域的伟大发明,绝对不超过二十项。这些包括分组交换网络、WEB、lisp、哈希算法、UNIX、编译技术、关系模型、面向对象、XML这些大名鼎鼎的家伙 ——只关注第一句就好。

一、缓存能提高性能的原因

        开发过系统的人都知道,使用缓存之后系统的性能将得到很大程度上的提升,为什么使用缓存之后就能提高系统的响应速度呢?我以为有以下三点:

1)高速的存储介质 

        持久化的存储介质比如数据库、文件,虽然数据存储安全,但有时速度较慢,于是我们选择更快的存储介质比如RAM(内存),这里有个来自Google大神Jeff Dean的基准测试图,大家感受下:ps:真实的带小方块的那个图更直观

存储介质速度
L1 cache reference 读取CPU的一级缓存0.5 ns
Branch mispredict(转移、分支预测)5 ns
L2 cache reference 读取CPU的二级缓存7 ns
Mutex lock/unlock 互斥锁\解锁100 ns
Main memory reference 读取内存数据100 ns
Compress 1K bytes with Zippy 1k字节压缩10,000 ns
Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节20,000 ns
Read 1 MB sequentially from memory 从内存顺序读取1MB250,000 ns
Round trip within same datacenter 从一个数据中心往返一次,ping一下500,000 ns
Disk seek 磁盘搜索10,000,000 ns
Read 1 MB sequentially from network从网络上顺序读取1兆的数据10,000,000 ns
Read 1 MB sequentially from disk 从磁盘里面读出1MB30,000,000 ns
Send packet CA->Netherlands->CA 一个包的一次远程访问150,000,000 ns

从上图可以看出,内存的访问速度大约是磁盘的100~10w倍。

2) 近距离而不是远距离

        对于读数据来说,就是说要把数据放到距离使用者最近的地方,减少中间的传输时间。例如视频app的视频缓存(可能采用某种压缩算法压缩了)、CDN加速等。

        这种方式很直观,比如生活中我们做某件事的时候,时常要用到事务A,与其用到的时候跑一个来回拿来用,不如直接放到身边,伸手就能够着。所以说啊,艺术源于生活。

3)存储计算的结果

        比如某些数据是经过复杂计算后得到的,我们直接把计算结果存起来,下次直接访问上次的计算结果就好了,例如Vue实例的computed属性(计算属性)。这样既能挺高获取速度,又能减少CPU的计算,用空间换时间,多好。

值得注意的是,这三种方式可以随意组合使用,具体要看你的使用场景。

缓存不能万能的

        缓存确实是提供系统性能的最简单效果也最好的方式,但缓存也不是万能的,引入缓存会提升系统的复杂度,玩不好的话也会带来很多问题,比如debug难度增加了,数据出现脏读了。

        所以啊当你的系统出现慢响应时,首先应看一下是不是因为访问量过大,达到了你的数据库的性能瓶颈:如果不是的话,很可能是因为程序中操作不当导致的系统变慢,这时你应该找到变慢原因并优化程序(具体操作具体分析),而不是直接引入缓存,这是治标不治本的方式;如果真的达到了存储组件的性能瓶颈,不应该再对数据库进行优化了,此时直接上缓存是个明智的选择。

二、常见的缓存使用模式

1、暂存(cache-aside pattern)

        即将数据库中的数据暂时存储在缓存中,访问数据时先读取缓存,若缓存中有直接返回;缓存中没有再读取数据库,然后同步数据到缓存。

        写操作时先更新数据库,再删除缓存。缓存的位置是处在数据库之前,起到了加速读取和保护数据库的作用。

2、只有高速缓存(cache-as-sor)

        去掉数据库,让缓存来充当数据库的角色,所有的操作全在缓存中进行。

        ps: sor 是 System of record(记录系统)的缩写

3、直写(write-through)

        当发生写操作时,所有的写操作全都在一个线程中串行执行。保证了数据的实时一致性,但每个写操作的响应时间会加长,总的降低了系统的吞吐量。

4、后写(write-behind)

        当发生写操作时,将一些写操作异步执行,比如对于复杂的写操作另起一个线程去异步执行,或者像剩余操作放到消息队列里面,后续异步执行。后写不能保证数据的实时一致性,但可以保证数据的最终一致性,同时每个写操作的响应时间会缩短,总的提高了系统吞吐量。

        关于缓存无非就是读和写,以上四种基本涵盖了缓存读写所有的情况,关于选用直写还是后写,可以从数据的实时一致性和系统的访问量上来综合考虑,针对具体的情况灵活运用即可。

三、常见的缓存组件及其使用分析

        从缓存所在的位置、是否是个独立的进程等方面来划分的话,缓存可分为本地缓存和远程缓存(又叫分布式缓存 )。

本地缓存有很多解决方案,java中常见的会有Ehcache、Caffeine、GuavaCache、OSCache、CurrentHashMap自定义等。

分布式缓存也有很多,如常见的Redis、Memcache等。

3.1 Ehcache

ehcache名字起的很好。

ehcache是用java写的一个简单易用的本地缓存库,他默认将数据存储在JVM的heap内存中,当超过一定量后,可以根据设置将数据存到硬盘中,理论上容量不用担心。像一些著名的持久层框架如Hibernate、mybatis都内置的Ehcache来缓存数据。

除此之外,Ehcache还具有一下特点:

1)数据可以设置过期时间

2)采用多种算法策略支持数据超量后的淘汰

3)支持数据后写模式

4)支持条件查询API等

以上对于一个本地缓存来说已经够用了。

缓存似乎很抽象、不直观,但其实他也是符合某种数据模型,像Ehcache的数据模型大致长这样:

 注意事项:

        由于Ehcache默认将数据存到JVM内存中,所以在配置Ehcache时注意不要超出了JVM的内存容量,要给你的程序预留一些内存,不然JVM可能就把OOM甩你脸上。

关于怎么样在程序中使用Ehcache构建缓存,可以参考这篇文章:

1、ehcache简介与HelloWorld_>no problem<的博客-CSDN博客

3.2 Redis

        redis是个性能强劲的key-value数据库(只用它来做缓存有点浪费了),提供了很多数据结构以及基于这些数据结构的算法命令,为我们的程序提供了很大的扩展性。例如:string、hash、列表、集合、有序集合、bitmap、Hyperloglog、以及简单的geo、发布订阅、以及组合命令的Lua脚本等,能做啥就看你想象力了,咱们这里只谈论redis做缓存的方式。

        用redis做缓存,我们是利用他的内存存储介质,并且是属于独立的服务与应用程序解耦。

3.2.1 编程式的方式直接操作redis

        一般来说,这种分布式服务都有他对应语言的客户端,redis在java中的客户端有老牌的jedis、新秀lettuce(生菜,springBoot2内置的)、分布式的客户端redission等,我们可以基于这些客户端做一些简单的封装,使用。

3.2.2 基于注解的方式使用redis

        spring对常用的缓存组件做了抽象,并用这些抽象来支持其提供的缓存注解。默认情况下,缓存实现为ConcurrentHashMap,基于springBoot热拔插的思想,我们只需要引入springBoot提供的

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

redis-starter就能很方便的将缓存注解的支持变成redis。

缓存注解如下:

@Cacheable(cacheNames = "缓存对象名称",key = "#id")    读取指定的缓存

@CachePut(cacheNames = "缓存对象名称",key = "#id")     写指定的缓存

@CacheEvict(cacheNames = "缓存对象名称",key = "#id")   过期指定的缓存

@Caching({})    组合多个缓存注解

关于如何在springBoot中使用redis、如何使用spring提供的缓存注解、以及如何将缓存注解的支持变成redis等相关问题,可以参看这篇文章:

springBoot2.x整合redis_>no problem<的博客-CSDN博客

这里简单说一下springBoot切换缓存注解实现的原理:

        springboot内部有个在org.springframework.boot.autoconfigure.cache包下有个CacheConfigurations类,定义了多种缓存组件的加载顺序,IOC容器启动时通过@ConditionalOnMissingBean 和@ConditionalOnBean 依次检测容器中是否存在指定的工厂类,若存在就会创建对应的缓存实现组件,最后都不存在则使用默认的。因为RedisCache的加载顺序是在默认的之前的,所以注入上面的starter即可替换默认的缓存实现。其实很简单,有点符合if-else if-else形式,只要有符合要求的,直接返回的意思。

3.3 GuavaCache

        GuavaCache是google.common.cache包下的一个本地缓存实现,是在ConcurrentHashMap的基础上提供了缓存获取和淘汰等策略,数据也是存在JVM堆内存中的,简单点的缓存需求可以使用这个。

3.4 Caffeine

        Caffeine也是个本地缓存,号称“本地缓存之王”,最大的特点就是性能高。据说他借鉴了GuavaCache和ConcurrentHashMap的设计,优化了数据结构和算法,从而变得高性能,一些常见的框架如play、spring、microNut都使用了他。

3.5 自定义简单缓存

        我们可以使用java提供的数据集合如ArrayList、ConcurrentHashMap,自己做一个简单的缓存容器也可以。

3.6 Memcache

        Memcache是一个分布式缓存组件,相比于redis来说,memcache是多线程的处理方式,所以不怕阻塞问题,其数据结构只有key-value形式,不过人家只是为了做缓存,key-value也够用了。

四、缓存使用过程中的一些经典问题

        缓存击穿、缓存穿透、缓存雪崩,危害程度是逐步提升的。但前提是当你的数据库达到了性能瓶颈,引入缓存来缓解数据库的压力的时候,出现这三个问题对系统的稳定性危害很大,如果都没达到数据库的性能瓶颈,即使这三个问题出现,也只能说是缓存使用不当,并不会对系统稳定性带来多大影响。

4.1 缓存击穿

4.2 缓存穿透

4.3 缓存雪崩

4.4 缓存预热

4.5 缓存降级

4.6 如果保证数据库和缓存中数据的双写一致?

以上6个问题的介绍和解决方案,可以参考我的这篇文章,这里就不再重复了:

缓存设计和使用中的经典问题_>no problem<的博客-CSDN博客

4.7 如何保证缓存中的数据都是最热的数据?

解决:

方式一:

        最简单直接的方式是,每次访问某个缓存key时,如果存在则重新设置过期时间。这样在过期时间内访问就永不过期,过期时间内不访问就被清除了。这和session的过期机制是一样的。但这样会造成双倍的操作,太直接了不太好。

方式二:

        我们可以异步来做,搞一个工作进程、一个守护进程。守护进程负责对工作进程产生的数据进行操作,比如给每个缓存数据设置一个lastAccessTime的时间戳,每次访问该数据的时候,由缓存服务器端异步更新该时间戳(客户端是无感知的),守护进程启动一个定时任务的线程,按照某种策略如:对所有的数据按时间戳升序排序,当达到指定的容量后,删除总数量的15%的数据,这样能保证不过期的数据中只保留最热的。

        对于过期了的数据,就交给缓存服务端来删除,至于是怎么检测过期的,客户端不用管。

        所以,一个优秀的缓存服务应该提供以上两点功能,先有自己的默认值,然后提供一些配置让使用者可选择。在以上三中介绍的缓存组件中,redis、ehcache、memcache、GuavaCache、caffeine都提供了这些功能,并且他们都采用了几乎相同的数据淘汰算法,例如LRU、LFU、FIFO。

LRU(Last Recently Use,最近最少使用),简单来说就是根据最后访问时间戳来删除。

LFU(Last Frequently Use,最近最频繁使用),简单来说就是根据访问次数属性来删除。

FIFO(First In First Out,先进先出),数据入队,先存储的先删除。

        如果面试官问你,如何保证redis中的数据都是最热的?

        多半他想知道的就是redis服务器端对于未过期数据的逐出策略,这些策略就是说的上面的这几种算法。 

        好了,关于详细的实施步骤文中给了链接,方便大家拿来参考。本文只是给出了关于缓存使用中的一些理论,具体在系统中实施缓存时还会遇到很多问题,不过有了这些理论,咱也有了研究的方向了不是。

        希望本文能给大家带来帮助,祝大家的系统性能都是蹭蹭蹭。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值