本文共 9709 字,大约阅读时间需要 32 分钟。
在计算中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据的主存储位置快。通过缓存,可以高效地重用之前检索或计算的数据。缓存本质上是以空间换时间,提高接口的响应速度。
是指一块芯片被集成到硬盘或者CPU上,作用就是充当硬盘,CPU与外界的接口之间的一个暂存器。这里的外界接口通常是指硬盘和内存,或者是CPU与内存之间。利用缓存可以减轻系统的负载,同时提高传播效率。
为了实现可以快速地响应用户的请求,会把之前用户浏览的信息,比如:一些图片,视频,css样式等,存在本地,在下次访问的时候,如果本地缓存有这些内容的话就会直接展示出来,不会再像我们服务器端发送新的请求。
与客户端缓存的目的是相同的,不过是站在服务器这边的一个考虑,如果每次用户的请求都要连接一次数据库的话,那当用户的请求数很多的时候,服务器的后台压力就会非常大。可以把一些经常被请求的数据放入内存中,当有请求到来的时候就可以直接返回,不用再次访问数据库,就会降低服务器的一个压力。
对缓存的常用操作描述
查询时,先读取缓存,如果缓存中没有数据,则触发真正的数据获取,如果缓存中有数据,直接返回缓存中的数据;
新增数据时,将数据写入缓存;
删除数据时,删除对应的缓存数据。并且可以自定义每个KEY的缓存有效期。
数据库的并发量大:读写分离+缓存
数据库的数据量大:分库分表
因为内存速度远远高于磁盘,所以从内存缓存中读取数据是非常快的,大大提升了数据访问速度。
缓存实例可以取代大量数据库实例,从而降低总成本
通过读取负载的重要部分从后端数据库中,定向到内存层,缓存可以降低数据库上的一个负载,防止其在负载情况下性能下降,甚至可以防止在高峰期的时候雪崩。
现代应用程序的一个挑战是应用程序使用的高峰时的一个处理时间,比如双十一时候,电子商务网站会不断在增加负载,会使获取数据时的时间延长,从而使总体的应用程序性能不可预测,通过应用高吞吐量的内存缓存可以缓解此问题。
在许多应用程序中,可能只有很少一部分的数据会被经常访问,会在你的数据库中产生热点,并可能根据最常用的数据的吞吐,要求过多地预支数据库中的资源,通过在内存缓存中缓存常用的数据,就不需要为最常访问的数据过度去预置,并能够提供快速的可预测性。
相比于同等的基于磁盘的数据库,除了更低的延迟外,内存系统还提供了更高的请求率,用作分布式这种类似的缓存的话,单个实例可以处理是十万个请求,例如rides。
缓存通常设置有效期,过期后应当失效,常见的过期策略有:定时、定期、惰性失效。
缓存占用有空间上限,超过上限需淘汰部分缓存数据,常见的淘汰策略有:FIFO(基于队列实现LinkedHashMap)、LRU、LFU。
缓存需要支持并发的读取写入。
请求数据中不存在的数据,导致每次都无法从缓存中命中,继而访问到数据库。
缓存失效的同时大量相同请求穿过缓存访问到数据。
大量缓存同时失效,导致大量请求穿过缓存访问到数据库。
HashMap、ConcurrentHashMap
缓存命中率;
缓存容量空间;
缓存清除策略;
不经常变动,数据量不大,但是有使用非常频繁的数据,就可以考虑放在本地缓存;
前后端分离:负载均衡策略,iphash,多级缓存的使用,先查本地缓存,redis,db。
分布式缓存+本地缓存=多级缓存
多级缓存框架:J2Cache(红薯) gitee.com
Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了 Java虚拟机的垃圾回收机制。
Guava Cache与 ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。Guava Cache 为了限制内存占用,通常都设定为自动回收元素。
适用场景:
Guava cache是运行在JVM的本地缓存,并不能把数据存放到外部服务器上。如果有这样的要求,应该尝试Mencached或Redis这类分布式缓存。
LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。
1.设置缓存容量;2.设置超时时间;3.提供移除监听器;4.提供缓存加载器;5.构建缓存
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K,Callable<V>)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
maximum Size(long)
:当缓存中的元素数量超过指定值时。
expireAfterAccess(long,TimeUnit)
:缓存项在给定时间内没有被读/写访问,则回收。
CacheBuilder.weakKeys()
:使用弱引用存储键。当key没有其他引用时,缓存项可以被垃圾回收。
CacheBuilder.weakValues()
:使用弱引用存储值。当value没有其他引用时,缓存项可以被垃圾回收。
CacheBuilder.softValues()
:使用软引用存储值。按照全局最近最少使用的顺序回收。
任何时候,你都可以显示地清除缓存项,而不是等到它被回收:
Cache.invalidate(key)
Cache.invalidateAll(keys)
Cache.invalidateAll()
CacheBuilder.recordStats()
:用来开启Guava Cache的统计功能。统计打开后,
Cache.stats()
方法会返回CacheStats 对象以提供如下统计信息:
hitRate()
:缓存命中率;averageLoadPenalty()
:加载新值的平均时间,单位为纳秒;evictionCount()
:缓存项被回收的总数,不包括显式清除。EhCache是一个用Java实现的,使用简单高速,实现线程安全的缓存管理类库;EhCache提供了用内存、磁盘、文件存储,以及分布式存储方式的多种零和的Cache管理方案,同时也是Hibernate中默认的CacheProvider。EhCache具有快速、简单、低消耗、依赖性小、扩展性强,支持对象序列化缓存,支持缓存和元素的失效,提供LRU,LFU和FIFO的一个缓存策略。
核心:CacheManager和Cache Manager Listener SPI
cache元素的属性:
name
:缓存名称maxElementsInMemory
:内存中最大缓存对象数maxElementsOnDisk
:硬盘中最大缓存对象数,若是0表示无穷大eternal:true
表示对象永不过期,此时会忽略timeToIdleSeconds
和timeToLiveSeconds
属性,默认为falsetimeToIdleSeconds
: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds
属性值,这个对象就会过期,EhCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态timeToLiveSeconds
:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了timeToLiveSeconds
属性值,这个对象就会过期,EhCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds
必须大于timeToIdleSeconds
属性,才有意义overflowToDisk:true
表示当内存缓存的对象数目达到了maxElementsInMemory
界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。diskSpoolBufferSizeMB
:磁盘缓存区大小,默认为30 MB。每个Cache都应该有自己的一个缓存区。diskPersistent
:是否缓存虚拟机重启期数据diskExpiryThreadIntervalSeconds
:磁盘失效线程运行时间间隔,默认为120秒memoryStoreEvictionPolicy
:当达到maxElementsInMemory
限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。Caffeine是Google基于Java 8对Guava Cache的重写升级版本,支持丰富的缓存过期策略,尤其是TinyLfu淘汰算法,提供了一个近乎最佳的命中率。从性能上(读、写、读/写)也足以秒杀其他一堆进程内缓存框架。Spring 5更是直接放弃了使用多年的Guava,而采用了Caffeine。
Caffeine的API的操作功能和Guava是基本保持一致的,并且Caffeine为了兼容之前是Guava的永华,做了一个Guava的Adapter给大家使用。
Caffeine是一个非常不错的缓存框架,无论是在性能方面,还是在API方面,都要比Guava cache要好一些。如果在新的项目中要使用local cache的话,可以优先考虑使用Caffeine。对于老的项目,如果使用了Guava cache,想要升级为Caffeine的话,可以使用Caffeine提供的Guava cache适配器,进行切换。
手动加载:Cache<String, Object>
,同步加载:LoadingCache<String, Object>
和异步加载:AsyncLoadingCache<String, Object>
。 同步加载和手动加载的区别就是在构建缓存时提供一个同步的加载方法。异步手动加载返回的不是值,而是 CompletableFuture
。
maximumSize(20)
:缓存最大条数,超过这个条数就是驱逐缓存maximumWeight(100)
:缓存最大权重值expireAfterWrite(2000, TimeUnit.MILLISECONDS)
:基于时间失效->写入之后开始计时失效;或者**expireAfterAccess(10, TimeUnit.SECONDS)
**:基于时间失效->访问之后开始计时失效比较项 | ConcurrentHashMap | Ehcache | Guava Cache | Caffeine |
---|---|---|---|---|
读写性能 | 很好,分段锁 | 好 | 好 | 很好 |
淘汰算法 | 无 | LRU,LFU,FIFO | LRU | W-TinyLfu |
功能丰富度 | 功能简单 | 功能丰富 | 功能丰富,支持刷新和虚引用 | 和Guava Cache类似 |
工具大小 | jdk自带,很小 | 一般 | 较小 | 一般 |
是否持久化 | 否 | 是 | 否 | 否 |
是否支持集群 | 否 | 是 | 否 | 否 |
缓存淘汰算法的作用是在有限的资源内,尽可能识别出哪些数据在短时间会被重复利用,从而提高缓存的命中率。常用的缓存淘汰算法有LRU、LFU、FIFO等。
TinyLFU维护了近期访问记录的频率信息,作为一个过滤器,当新记录来时,只有满足TinyLFU要求的记录才可以被插入缓存。如前所述,作为现代的缓存,它需要解决两个挑战:一个是如何避免维护频率信息的高开销,另一个是如何反应随时间变化的访问模式。首先来看前者,TinyLFU借助数据流Sketching技术,Count-Min Sketch显然是解决这个问题的有效手段,它可以用小得多的空间存放频率信息,而保证很低的False Positive Rate。但考虑到第二个问题,就要复杂许多了,任何Sketching数据结构如果要反应时间变化都是一件困难的事情,在Bloom Filter方面,我们可以有Timing Bloom Filter,但对于CMSketch来说,如何做到Timing CMSketch就不那么容易了。TinyLFU采用了一种基于滑动窗口的时间衰减设计机制,借助于一种简易的reset操作:每次添加一条记录到Sketch的时候,都会给一个计数器上加1,当计数器达到一个尺寸W的时候,把所有记录的Sketch数值都除以2,该reset操作可以起到衰减的作用。
W-TinyLFU主要用来解决一些稀疏的突发访问元素。在一些数目很少但突发访问量很大的场景下,TinyLFU将无法保存这类元素,因为它们无法在给定时间内积累到足够高的频率。因此W-TinyLFU就是结合LFU和LRU,前者用来应对大多数场景,而LRU用来处理突发流量。
前端是一个小的LRU,在送到TinyLFU做过滤之后,元素存放到一个大的Segmented LRU缓存里。前端的小LRU叫做Window LRU,它的容量只占据1%的总空间,它的目的就是用来存放短期突发访问记录。存放主要元素的Segmented LRU(SLRU)是一种LRU的改进,主要把在一个时间窗口内命中至少2次的记录和命中1次的单独存放,这样就可以把短期内较频繁的缓存元素区分开来。具体做法上,SLRU包含2个固定尺寸的LRU,一个叫Probation段A1,一个叫Protection段A2。新记录总是插入到A1中,当A1的记录被再次访问,就把它移到A2,当A2满了需要驱逐记录时,会把驱逐记录插入到A1中。W-TinyLFU中,SLRU有80%空间被分配给A2段。
Caffeine的load,put和invalidate操作都是原子的,这个意思是这3个操作时互斥的,load和put是不能同时执行的,load和invalidate也是不能同时执行的。先load再invalidate,invalidate操作是要等load操作执行完的。如果load操作执行比较慢,那invalidate操作就要等很久了。
**caffeine的存储就是ConcurrentHashMap,利用了ConcurrentHashMap自己的node节点锁。**invalidate操作对应的就是remove方法,load方法对应的是compute方法,remove和compute方法都是加锁的,key相同的情况下,remove和compute方法加的是相同的锁。
Timer
和ScheduledExecutorService
@Scheduled(cron="0 0 0/1 * *?")//每隔一小时更新
缓存穿透是指查询一个一定不存在的数据,由于缓存时不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,引起数据库压力瞬间增大,造成过大压力。
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
@Cacheable
:主要针对方法配置,能够根据方法的请求参数对其进行缓存;
@CacheEvit
:清空缓存;
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
@Cacheable
:主要针对方法配置,能够根据方法的请求参数对其进行缓存;
@CacheEvit
:清空缓存;
@CachePut
:保证方法被调用,又希望结果被缓存与@Cacheable
区别在于是否每次都调用方法,常用于更新
转载地址:http://mmugn.baihongyu.com/