博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
缓存优化
阅读量:3925 次
发布时间:2019-05-23

本文共 9709 字,大约阅读时间需要 32 分钟。

1、了解内存缓存

1. 1、什么是缓存

在计算中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据的主存储位置快。通过缓存,可以高效地重用之前检索或计算的数据。缓存本质上是以空间换时间,提高接口的响应速度。

硬件

是指一块芯片被集成到硬盘或者CPU上,作用就是充当硬盘,CPU与外界的接口之间的一个暂存器。这里的外界接口通常是指硬盘和内存,或者是CPU与内存之间。利用缓存可以减轻系统的负载,同时提高传播效率。

客户端缓存

为了实现可以快速地响应用户的请求,会把之前用户浏览的信息,比如:一些图片,视频,css样式等,存在本地,在下次访问的时候,如果本地缓存有这些内容的话就会直接展示出来,不会再像我们服务器端发送新的请求。

服务器端缓存

与客户端缓存的目的是相同的,不过是站在服务器这边的一个考虑,如果每次用户的请求都要连接一次数据库的话,那当用户的请求数很多的时候,服务器的后台压力就会非常大。可以把一些经常被请求的数据放入内存中,当有请求到来的时候就可以直接返回,不用再次访问数据库,就会降低服务器的一个压力。

1.2、 使用缓存

  • 对缓存的常用操作描述

    查询时,先读取缓存,如果缓存中没有数据,则触发真正的数据获取,如果缓存中有数据,直接返回缓存中的数据;

    新增数据时,将数据写入缓存;

    删除数据时,删除对应的缓存数据。并且可以自定义每个KEY的缓存有效期。

数据库的并发量大:读写分离+缓存

数据库的数据量大:分库分表

1.3、缓存使用的常见场景

  1. 高并发查询:例如用户在客户端使用脚本定期批量查询商品库存。
  2. 热点查询:例如大批量库存查询请求集中于少部分热门商品。
  3. 高并发写入:例如库存充足时,大量并发下单请求。
  4. 热点写入:例如库存充足时,针对少部分热门商品大量并发下单请求。
  5. 大对象初始化

1.4、 使用缓存的好处

  • 提高应用程序性能

因为内存速度远远高于磁盘,所以从内存缓存中读取数据是非常快的,大大提升了数据访问速度。

  • 降低数据库成本

缓存实例可以取代大量数据库实例,从而降低总成本

  • 减少后端负载

通过读取负载的重要部分从后端数据库中,定向到内存层,缓存可以降低数据库上的一个负载,防止其在负载情况下性能下降,甚至可以防止在高峰期的时候雪崩。

  • 可预测的性能

现代应用程序的一个挑战是应用程序使用的高峰时的一个处理时间,比如双十一时候,电子商务网站会不断在增加负载,会使获取数据时的时间延长,从而使总体的应用程序性能不可预测,通过应用高吞吐量的内存缓存可以缓解此问题。

  • 消除数据库热点

在许多应用程序中,可能只有很少一部分的数据会被经常访问,会在你的数据库中产生热点,并可能根据最常用的数据的吞吐,要求过多地预支数据库中的资源,通过在内存缓存中缓存常用的数据,就不需要为最常访问的数据过度去预置,并能够提供快速的可预测性。

  • 提高读取吞吐量(IOPS):

相比于同等的基于磁盘的数据库,除了更低的延迟外,内存系统还提供了更高的请求率,用作分布式这种类似的缓存的话,单个实例可以处理是十万个请求,例如rides。

1.5、缓存的常用类型

  • 内存缓存(进程内缓存,性能最好)
  • 分布式缓存
  • 组合缓存(多级缓存)

1.6、 缓存的特点

  • 设置存活时间(过期策略)

缓存通常设置有效期,过期后应当失效,常见的过期策略有:定时、定期、惰性失效

  • 空间占用有限(淘汰策略)

缓存占用有空间上限,超过上限需淘汰部分缓存数据,常见的淘汰策略有:FIFO(基于队列实现LinkedHashMap)、LRU、LFU

  • 支持并发更新

缓存需要支持并发的读取写入。

1.7、缓存使用时的常见问题

  • 缓存穿透

请求数据中不存在的数据,导致每次都无法从缓存中命中,继而访问到数据库。

  • 缓存击穿

缓存失效的同时大量相同请求穿过缓存访问到数据。

  • 缓存雪崩

大量缓存同时失效,导致大量请求穿过缓存访问到数据库。

1.8、 常用内存缓存的实现方式

  • Java容器 :基于JDK自带的Map容器类HashMap、ConcurrentHashMap
  • Guava Cache : Google提供的Java增强工具包Guava的一个模块,社区活跃
  • Ehcache :重量级的内存缓存,支持2级缓存,Hibernate中默认的缓存框架
  • Caffeine :基于Guava API开发的高性能内存缓存,Spring 5默认的内存缓存框架

1.9 缓存的几个关键要素

  1. 缓存命中率;

  2. 缓存容量空间;

  3. 缓存清除策略;

    • FIFO:最少使用策略
    • LRU:最近最少使用策略
    • LFU:先进先出策略

1.10 分布式缓存与本地单机缓存

不经常变动,数据量不大,但是有使用非常频繁的数据,就可以考虑放在本地缓存;

前后端分离:负载均衡策略,iphash,多级缓存的使用,先查本地缓存,redis,db。

分布式缓存+本地缓存=多级缓存

多级缓存框架:J2Cache(红薯) gitee.com

2、常见开源内存缓存工具介绍

2.1、Guava Cache

2.1.1、 Guava Cache简介

Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了 Java虚拟机的垃圾回收机制

Guava Cache与 ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。Guava Cache 为了限制内存占用,通常都设定为自动回收元素。

适用场景:

  1. 愿意消耗一些内存空间来提升速度。
  2. 预料到某些键会被多次查询。
  3. 缓存中存放的数据总量不会超出内存容量。

Guava cache是运行在JVM的本地缓存,并不能把数据存放到外部服务器上。如果有这样的要求,应该尝试Mencached或Redis这类分布式缓存。

2.1.2、 Guava Cache 加载

  • 加载方式1 - CacheLoader

LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。

1.设置缓存容量;2.设置超时时间;3.提供移除监听器;4.提供缓存加载器;5.构建缓存

  • 加载方式2 - Callable

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K,Callable<V>)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。

2.1.3、 Guava Cache 缓存回收

  1. 基于容量的回收

maximum Size(long):当缓存中的元素数量超过指定值时。

  1. 定时回收

expireAfterAccess(long,TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。

  1. 基于引用回收(Reference-based Eviction)

CacheBuilder.weakKeys():使用弱引用存储键。当key没有其他引用时,缓存项可以被垃圾回收。

CacheBuilder.weakValues():使用弱引用存储值。当value没有其他引用时,缓存项可以被垃圾回收。

CacheBuilder.softValues():使用软引用存储值。按照全局最近最少使用的顺序回收。

2.1.4、 Guava Cache 显示清除

任何时候,你都可以显示地清除缓存项,而不是等到它被回收:

  1. 个别清除:Cache.invalidate(key)
  2. 批量清除:Cache.invalidateAll(keys)
  3. 清除所有缓存项:Cache.invalidateAll()

2.1.5、Guava Cache 统计

CacheBuilder.recordStats():用来开启Guava Cache的统计功能。统计打开后,

Cache.stats()方法会返回CacheStats 对象以提供如下统计信息:

  • hitRate():缓存命中率;
  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
  • evictionCount():缓存项被回收的总数,不包括显式清除。

2.2、Ehcache

EhCache是一个用Java实现的,使用简单高速,实现线程安全的缓存管理类库;EhCache提供了用内存、磁盘、文件存储,以及分布式存储方式的多种零和的Cache管理方案,同时也是Hibernate中默认的CacheProvider。EhCache具有快速、简单、低消耗、依赖性小、扩展性强,支持对象序列化缓存,支持缓存和元素的失效,提供LRU,LFU和FIFO的一个缓存策略。

2.2.1、主要特性

  1. 快速、简单、支持多种缓存策略;
  2. 支持内存和磁盘缓存数据,因此无需担心容量问题;
  3. 缓存数据会在虚拟机重启的过程中写入磁盘;
  4. 可以通过RMI、可插入API等方式进行分布式缓存(比较弱);
  5. 具有缓存和缓存管理器的侦听接口;
  6. 支持多缓存管理器实例,以及一个实例的多个缓存区域;
  7. 提供Hibernate的缓存实现。

2.2.2、 EhCache架构图

在这里插入图片描述

核心:CacheManager和Cache Manager Listener SPI

2.2.3、 适用场景

  1. 单个应用或者对缓存访问要求很高的应用;
  2. 简单的共享可以,但是不适合设计缓存恢复、大数据缓存;
  3. 大型系统,存在缓存共享、分布式部署、缓存内容大不适合使用;
  4. 在实际工作中,更多是将Ehcache作为与Redis配合的二级缓存;

2.2.4、 Cache的参数属性

cache元素的属性:

  • name:缓存名称
  • maxElementsInMemory:内存中最大缓存对象数
  • maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
  • eternal:true表示对象永不过期,此时会忽略timeToIdleSecondstimeToLiveSeconds属性,默认为false
  • timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了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(最少访问次数)

2.3、Caffeine

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适配器,进行切换。

2.3.1、 Caffeine的三种加载方式

手动加载:Cache<String, Object>,同步加载:LoadingCache<String, Object>和异步加载:AsyncLoadingCache<String, Object> 同步加载和手动加载的区别就是在构建缓存时提供一个同步的加载方法。异步手动加载返回的不是值,而是 CompletableFuture

2.3.2、 Caffeine的三种驱逐策略

  1. 基于缓存大小maximumSize(20):缓存最大条数,超过这个条数就是驱逐缓存
  2. 基于权重大小maximumWeight(100):缓存最大权重值
  3. 基于时间expireAfterWrite(2000, TimeUnit.MILLISECONDS):基于时间失效->写入之后开始计时失效;或者**expireAfterAccess(10, TimeUnit.SECONDS)**:基于时间失效->访问之后开始计时失效

2.4、 内存缓存对比

比较项 ConcurrentHashMap Ehcache Guava Cache Caffeine
读写性能 很好,分段锁 很好
淘汰算法 LRU,LFU,FIFO LRU W-TinyLfu
功能丰富度 功能简单 功能丰富 功能丰富,支持刷新和虚引用 和Guava Cache类似
工具大小 jdk自带,很小 一般 较小 一般
是否持久化
是否支持集群

3、Caffeine实现原理

3.1、淘汰算法

缓存淘汰算法的作用是在有限的资源内,尽可能识别出哪些数据在短时间会被重复利用,从而提高缓存的命中率。常用的缓存淘汰算法有LRU、LFU、FIFO等。

  • LRU(Least Recently Used) 算法认为最近访问过的数据将来被访问的几率也更高。在突发流量下表现良好。
  • LFU(Least Frequently Used) 算法根据数据的历史访问频率来淘汰数据,其核心思想是"如果数据过去被访问多次,那么将来被访问的频率也更高"。如果想要实现这个算法,需要一套额外的存储来存每个元素的访问次数,会造成内存资源的浪费。
  • caffeine淘汰算法 Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,其特点:高命中率、低内存占用。TinyLFU是一种为了解决传统LFU算法空间存储比较大的问题LFU算法,它可以在较大访问量的场景下近似的替代LFU的数据统计部分,它的原理有些类似BloomFilter。

在这里插入图片描述

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段。

3.2、 Caffeine核心类图

在这里插入图片描述

3.3、 Caffeine缓存的分类

在这里插入图片描述

3.4、 Caffeine操作的原子性

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方法加的是相同的锁。

4、应对使用缓存后带来的一系列问题

4.1、 缓存同步问题

在这里插入图片描述

4.2、 缓存同步问题解决方案

4.2.1、 数据实时同步

  • 这种数据同步是增量、主动、强一致性
    1. 对数据库数据进行更新的时候(新增、删除、更新)淘汰缓存(缓存失效)
    2. 读取数据的时候更新缓存,为了避免缓存击穿带来的雪崩问题我们需要做同步处理,控制只有一个线程去读取数据然后更新到缓存,其他线程被阻塞等待。
    3. 设置缓存失效时间,这是一个兜底操作假设在更新缓存失败,这个缓存失效时间一到就会把缓存失效。

在这里插入图片描述

4.2.2、 数据准实时同步

  • 这种数据同步是增量、被动、准一致性
    1. 对数据进行更新操作时再更新数据库后发送一个更新缓存的MQ消息(如果要保证数据不丢失,建议可以建立本地一个消息表在发送MQ失败后可以重试)
    2. 缓存更新服务消费MQ更新数据消息后读取数据库数据进行相关业务处理。
    3. 缓存更新服务更新业务处理结果数据到缓存中。

在这里插入图片描述

4.2.3、 任务调度更新

  • 这种通过分布式调度任务进行定时更新缓存,使用场景如:报表统计数据、对账数据定时更新到缓存等 实时性要求不高的场景。
  • 实现比较简单
    1. Java中的TimerScheduledExecutorService
    2. Spring Task定时任务@Scheduled(cron="0 0 0/1 * *?")//每隔一小时更新
    3. 定时任务框架Quartz

在这里插入图片描述

4.2.4、 binlog日志订阅

  • 通过订阅binlog来更新缓存,把我们搭建的消费服务,作为mysql的一个slave,订阅binlog,解析出更新内容,再更新到缓存。性能会比使用定时任务要好很多,数据的一致性也能够更好地得到保障。和业务完全解耦,解决了持续性问题。但是成本会增大。

在这里插入图片描述

4.3 、 缓存穿透、缓存击穿、缓存雪崩

4.3.1、 缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存时不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

  • 解决方案
    • 针对业务场景对请求的参数进行有效性校验,防止非法请求击垮DB。如用户id不存在的(id<=0)直接拦截。
    • 如果DB查询不到数据,保存空对象到缓存层,设置较短的失效时间。
    • 采用bloom filter保存缓存过的key,在访问请求到来时可以过滤掉不存在的key,防止这些请求到DB层。

4.3.2、 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,引起数据库压力瞬间增大,造成过大压力。

  • 解决方案
    • 设置热点数据永不过期;如果缓存数据不设置失效时间的话,就不会存在热点key过期造成了大量请求到数据库。
    • 加互斥锁;当缓存数据失效时,保存一个请求能够访问到数据库,并更新缓存,其他线程等待并重试。

4.3.3、 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 解放方案
    • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
    • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
    • 设置热点数据永远不过期。

Spring Cache 提供缓存注解

@Cacheable:主要针对方法配置,能够根据方法的请求参数对其进行缓存;

@CacheEvit:清空缓存;

  • 加互斥锁;当缓存数据失效时,保存一个请求能够访问到数据库,并更新缓存,其他线程等待并重试。

4.3.3、 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 解放方案
    • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
    • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
    • 设置热点数据永远不过期。

Spring Cache 提供缓存注解

@Cacheable:主要针对方法配置,能够根据方法的请求参数对其进行缓存;

@CacheEvit:清空缓存;

@CachePut:保证方法被调用,又希望结果被缓存与@Cacheable区别在于是否每次都调用方法,常用于更新

转载地址:http://mmugn.baihongyu.com/

你可能感兴趣的文章
Nginx+tomct 动静分离实现负载均衡
查看>>
Uploadify 3.2 参数属性、事件、方法函数详解以及配置
查看>>
uploadify 上传图片附件问题
查看>>
JSP文件是否有必要放在WEB-INF下
查看>>
springMVC + hibernate jar整合 (包含log4j)
查看>>
oracle 语句修改字段类型
查看>>
Java开发中的23种设计模式详解
查看>>
mybaties 多个参数传参的三种解决方案
查看>>
Java 内存模型及GC原理
查看>>
Java内存模型及GC原理java内存模型 Java内存模型及GC原理 Java内存模型及GC原理 sun官方网站:sun java 虚拟机模型 Java内存模型及GC原理 JVM内存模型中分两
查看>>
java 结构型模式
查看>>
java 五大创建型模式
查看>>
java 六大原则
查看>>
java 常用五种排序
查看>>
java代码分析及分析工具
查看>>
Druid实现数据库连接用户密码加密
查看>>
Servlet - 会话跟踪
查看>>
Java内存区域与内存溢出(JVM)
查看>>
Java 虚拟机结构分析
查看>>
浅析 Redis 复制
查看>>