目录
文章目录
缓存
简介
缓存(Cache)是一种存储技术,用于临时存放从原始数据源(如硬盘、数据库或网络)获取的数据副本,目的是加快数据的访问速度,减少不必要的重复处理,进而提升系统整体的性能和响应效率。它是计算机科学中“空间换时间”策略的一个典型应用,即通过牺牲少量的存储空间来换取数据访问速度的显著提升。
简而言之,缓存就是存储数据副本或计算结果的组件,以便后续可以更快地访问。
工作原理
一句话概况:更快读写的存储介质+减少IO+减少CPU计算=性能优化。
- 数据存储:当系统首次请求数据时,数据从原始数据源加载,并被复制到缓存中。这一步通常发生在第一次访问或数据更新后重新加载时。
- 快速访问:之后,当系统再次请求相同的数据时,可以直接从缓存中获取,而无需再次访问较慢的数据源。由于缓存通常位于更快的存储介质上(如RAM),数据访问速度远高于直接从硬盘或网络获取。
- 数据更新与同步:缓存中的数据并不是永久不变的,需要有机制来维护其与数据源之间的一致性。常见的策略包括定时刷新、写穿(write-through)、写回(write-back)等。此外,为了高效利用有限的缓存空间,还会有算法(如最近最少使用LRU、最不经常使用LFU等)定期淘汰旧数据,为新数据腾出空间。
缓存分类
1.按照技术层次分类
- 硬件缓存
- CPU缓存:位于CPU和主内存之间,用于加速数据和指令的访问速度,减少CPU等待时间。
- GPU缓存:类似CPU缓存,专为图形处理单元设计,提高图形渲染和数据处理速度。
- 软件缓存
- 操作系统缓存:如文件系统缓存,用于加速文件的读写操作。
- 数据库缓存:数据库管理系统内部缓存,如MySQL的InnoDB缓冲池,缓存索引和数据页,减少磁盘I/O。
- Web应用缓存
- 浏览器缓存:存储已访问过的网页资源,如图片、样式表、JavaScript文件,加速页面加载。
- 代理服务器缓存:位于客户端和源服务器之间,存储常用网页内容,减少带宽消耗和响应时间。
2.按照应用场景分类
- 数据库查询缓存
- ORM工具缓存:如Hibernate、MyBatis的二级缓存,减少对数据库的重复查询。
- 搜索引擎缓存:加速搜索结果的呈现,如Elasticsearch的查询缓存。
- Web服务与API缓存
- RESTful API缓存:通过HTTP缓存机制(如ETag、Last-Modified)减少API响应时间。
- CDN缓存:内容分发网络,全球分布的缓存节点存储静态资源,提升访问速度和用户体验。
- 应用数据缓存
- Session缓存:存储用户的会话状态,减轻数据库负担,如Web应用中的用户登录状态。
- 计算结果缓存:如Spring Cache,用于存储昂贵计算的结果,避免重复计算。
- 分布式缓存
- Redis/Memcached:高性能键值存储系统,常用于分布式应用中共享数据缓存,提高数据访问速度和应用扩展性。
3.按照缓存策略分类
- 时间敏感型缓存
- LRU(Least Recently Used):最近最少使用策略,移除最近未被访问的数据。
- TTL(Time To Live):设置数据在缓存中的有效存活时间,到期自动删除。
- 访问频率敏感型缓存
- LFU(Least Frequently Used):最少访问次数策略,移除访问频率最低的数据。
- LFU变体(如LRU2、2Q):结合访问时间和频率进行淘汰。
- 自适应策略
- FBR(Frequency-Based Replacement)、LRUF(Least Recently and Frequently Used First)、ALRUF等,根据访问模式动态调整淘汰策略。
应用场景
1.硬件缓存
- CPU缓存:CPU级别的缓存分为L1、L2、L3等,用于存储频繁访问的指令和数据,减少CPU访问内存的时间。
- GPU缓存:用于存储图形处理过程中频繁访问的数据,加速图形渲染和计算任务。
2.软件缓存
数据库缓存
- 数据库查询缓存:例如MySQL的查询缓存,存储查询结果,减少对数据库的重复查询。
- ORM框架缓存:如Hibernate二级缓存、MyBatis二级缓存,提高数据访问速度。
Web开发
-
浏览器缓存:存储静态资源(如图片、CSS、JavaScript)以减少网络请求,加速页面加载。
-
Web服务器缓存:如Nginx缓存、Apache模块缓存,减少服务器对动态内容的重复生成。
-
CDN缓存:内容分发网络,全球分布存储网站内容,缩短用户访问距离,提高速度。
应用层缓存
- 会话缓存:存储用户会话信息,减轻数据库压力,如Tomcat session复制。
- 计算结果缓存:存储计算密集型操作的结果,如价格计算、推荐算法结果等。
- API响应缓存:缓存频繁请求的API响应,减少后端服务负载。
3.分布式缓存
- Redis:高可用的键值存储系统,用于存储会话信息、计数器、消息队列等。
- Memcached:简单高效的分布式缓存系统,适合存储简单的键值数据。
- Spring Cache:提供了一套抽象,方便在Spring应用中整合各种缓存技术。
4.微服务架构
- 服务间通信缓存:减少服务间重复的RPC调用,提高微服务间通信效率。
- 配置中心缓存:存储配置信息,确保配置更改时能快速传播到各服务实例。
5.移动端应用
- 离线缓存:为提高用户体验,在移动端应用中缓存数据,以便在网络不稳定或无网络时仍能访问内容。
6.大数据处理
- MapReduce缓存:在MapReduce作业中,预加载数据到内存,减少多次磁盘I/O。
- Spark缓存:将RDD(弹性分布式数据集)存储在内存中,加速迭代计算。
7.游戏开发
- 游戏资源缓存:减少游戏启动和运行时的加载时间,提升玩家体验。
通过上述场景可以看出,缓存的应用几乎覆盖了所有需要提高数据访问速度和降低系统响应时间的领域,是现代软件开发中不可或缺的技术。
缓存优点
- 提高性能:减少了对外部慢速资源的访问,提高了数据访问速度。
- 降低负载:减少了数据库、网络等资源的压力。
- 提升用户体验:页面加载更快,应用响应更迅速。
缓存带来的问题
- 数据一致性:确保缓存数据与数据源保持一致,避免脏读或数据不一致问题。
- 缓存失效:如何高效管理缓存项的生命周期,避免缓存雪崩、击穿等问题。
- 资源管理:合理分配缓存资源,避免过度占用内存。
- 缓存介质带来的不可靠性:(一般使用内存做缓存的话,若机器故障,如何保证缓存的高可用?可考虑对缓存进行分布式做成高可用,同时,需要接受这种不可靠不安全会给数据带来的问题,在异常情况下进行补偿处理,定期持久化等方式)
- 缓存的数据使得更难排查问题:因为缓存命中是随着访问随时变化的,缓存的行为难以重现,使得出现BUG很难排查。
- 进程内缓存可能会增加GC压力:在具有垃圾收集功能的语言中(如Java),大量长寿命的缓存对象会增加垃圾收集的时间和次数。
使用缓存之前我们需要对数据进行分类,对访问行为进行预估,思考哪些数据需要缓存,缓存时需要采用什么策略?这样,我们才不被缓存所困扰,才能规避这些问题。
常见常用Java缓存技术
1.Redis(最常用)
特点:Redis是一个开源的、基于键值对的数据结构存储系统,可用作数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,且数据全部存储在内存中,提供了极高的读写速度。Redis支持主从复制和集群模式,能够满足高可用和水平扩展的需求。
应用场景:适合需要高性能、低延迟的数据缓存和实时分析场景,如社交网络的点赞计数、购物车、实时排行榜等。
实际使用:在构建实时数据分析平台时,使用Redis作为缓存存储热点数据,结合发布/订阅功能实现实时数据推送,极大提升了数据处理和响应速度。
2.MyBatis缓存
2.1 MyBatis一级缓存
特点:
- 作用域:一级缓存是SqlSession级别的缓存,即每个SqlSession实例都有自己的缓存空间。
- 生命周期:与SqlSession相同,当SqlSession关闭时,一级缓存也随之失效。
- 自动启用:MyBatis默认开启一级缓存,无需额外配置。
- 自动管理:当执行查询后,结果自动存入缓存,后续相同查询直接从缓存中读取,直到SqlSession中发生增删改操作,缓存会被清空。
- 数据隔离:不同SqlSession之间缓存不共享,适用于单线程操作或短生命周期的SqlSession。
应用场景:
- 适用于单个事务内多次查询同一数据的场景,可以减少数据库访问,提升性能。
- 适合处理简单的查询操作,尤其是在数据不频繁变动且查询频繁的情况下。
实际使用:
- 开发者通常不需要做额外配置即可享受一级缓存带来的性能提升。
- 在需要确保数据最新时,可通过手动清除缓存(如调用SqlSession的clearCache()方法)或关闭当前SqlSession来重新查询数据库。
2.2 MyBatis二级缓存
特点:
- 作用域:二级缓存是Mapper级别的,跨SqlSession共享,由同一个SqlSessionFactory创建的所有SqlSession共享。
- 配置需求:需要手动开启,且需在对应的Mapper XML文件中使用标签配置。
- 数据一致性:二级缓存更加关注数据的一致性问题,通常需要序列化和反序列化对象,且在数据更新时(增删改)会清空相关缓存。
- 灵活性:提供了更多配置选项,如缓存实现类、缓存大小、过期策略等,可以自定义缓存策略。
应用场景:
- 适合多线程环境,或在多个SqlSession间共享数据的场景。
- 对于不经常改变的共享数据,如配置信息、静态数据表等,使用二级缓存可以显著提高系统性能。
实际使用:
- 需要在MyBatis配置文件中全局启用二级缓存,并在相应的Mapper配置文件中添加标签。
- 考虑到数据一致性,使用二级缓存时要确保数据的更新策略能及时刷新缓存,避免脏读。
- 实际开发中,可能还需要结合第三方缓存工具(如Redis)进一步增强二级缓存的功能,实现更复杂的数据共享和管理。
3.Spring Cache
特点:Spring Cache是Spring框架提供的一个抽象缓存支持,它不直接提供缓存实现,而是提供了一套接口,允许开发者灵活选择和配置不同的缓存提供商,如Ehcache、Redis、Caffeine等。通过注解的方式,开发者可以轻松实现方法级别的缓存。
应用场景:适用于基于Spring框架的应用,特别是那些需要细粒度控制缓存策略的应用,如数据访问层的方法缓存、Web服务的响应缓存等。
实际使用:在构建RESTful API时,使用Spring Cache注解来缓存频繁请求但不经常变化的API响应,减少了服务器的计算和数据库查询负担,提高了服务的响应速度和吞吐量。
4.Ehcache
特点:Ehcache是一种广泛使用的、轻量级的Java进程内缓存解决方案。它支持内存和磁盘两级缓存,提供了丰富的缓存策略(如LRU、LFU等),并且可以进行分布式缓存的扩展。Ehcache配置简单,易于集成到现有的Java应用中,支持事务、监听器和统计信息收集等功能。
应用场景:适用于单机应用或小型集群环境,特别适合需要频繁读取且不经常变更的数据缓存,如用户会话、配置信息等。
实际使用:在处理高并发用户会话管理时,Ehcache可以有效减少数据库的读取压力,通过设置合理的过期策略,保证数据的新鲜度同时减轻维护负担。
额外Java缓存解决方案
1.Guava Cache:
- 特点:Guava库提供的本地缓存实现,提供了丰富的缓存过期策略(基于时间、基于大小、基于引用等)、自动加载、统计信息等功能,且易于使用,适合单机应用。
- 应用场景:适用于需要高性能、轻量级缓存解决方案的Java应用程序,特别是在处理大量数据缓存和快速查找场景下。
2.Caffeine:
- 特点:Caffeine是一个高性能、近似最近最少使用(LRU)的本地缓存库,设计上旨在超越Guava Cache,提供了更优秀的性能和灵活性。它通过优化数据结构和算法,实现了低延迟和高命中率。
- 应用场景:适用于需要高性能缓存且对响应时间敏感的应用,如高访问量的Web应用、实时数据分析系统等。
3.OSCache:
- 特点:OSCache是一个成熟的开源缓存框架,支持缓存JSP页面、Servlet输出、对象图形等,提供了事件监听器、缓存刷新机制和灵活的配置选项。
- 应用场景:虽然不如Guava和Caffeine现代,但在一些遗留系统中仍可见其身影,特别适合需要页面缓存和动态内容缓存的Web应用。
4.Infinispan:
- 特点:Infinispan是一个高度可扩展的分布式缓存框架,不仅支持内存缓存,还支持持久化存储,提供了事务支持、数据网格功能,以及丰富的数据分片和复制策略。
- 应用场景:适用于需要分布式缓存解决方案的大规模应用,特别是在需要高性能、高可用和数据一致性的场景中。
5.Hazelcast:
- 特点:Hazelcast是一个开源的内存数据网格,提供了分布式的内存缓存、计算和消息传递解决方案。它支持多节点集群、数据分区、高可用性配置等高级特性。
- 应用场景:适合需要高性能、分布式缓存和数据共享的系统,如微服务架构、大数据处理、实时分析等。
6.ConcurrentHashMap / LRUHashMap:
- 特点:虽然不是专门的缓存框架,但通过自定义实现,可以基于JDK内置的ConcurrentHashMap或继承LinkedHashMap实现简单的LRU缓存。这种方式灵活但功能相对基础。
- 应用场景:适合轻量级缓存需求,或者作为临时解决方案,特别是在对第三方依赖有严格限制的项目中。
7.Memcached:
- 特点:虽然不是纯Java实现,但通过客户端库可以在Java应用中使用。Memcached是一个高性能、分布式内存对象缓存系统,简单易用,广泛应用于缓存数据库查询结果、会话数据等。
- 应用场景:适用于跨语言环境,特别是需要在不同应用或服务之间共享缓存数据的场景。
8.Apache JCS (Java Caching System):
- 特点:JCS是一个高性能、分布式缓存系统,提供了多种缓存算法,支持多种缓存后端(如内存、磁盘、远程缓存等),并支持缓存监听器、事件通知等。
- 应用场景:适合需要灵活配置和高级缓存管理功能的应用,尤其是在需要多种缓存策略和存储介质的应用中。
9.Hibernate Second Level Cache
- 特点:作为ORM框架Hibernate的一部分,第二级缓存用于缓存数据库查询结果,以减少数据库的访问次数。它可以与多种第三方缓存实现集成,如Ehcache、Infinispan等,提供了一种透明的缓存机制,开发者无需直接操作缓存。
- 应用场景:适用于基于Hibernate的大型应用程序,尤其是那些需要频繁访问相同数据库实体的场景,通过减少数据库的读取操作,提升整体性能。
常用Java缓存技术用法
Ehcache
简介:ehcache是现在非常流行的纯java开源框架,配置简单,结构清晰,功能强大。
实际使用
1.导入依赖
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2.yml配置
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
3.ehcache的xml配置
<ehcache
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--缓存路径-->
<diskStore path="D:/project/cache_demo/base_ehcache"/>
<!-- 默认缓存 -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- helloworld1缓存 -->
<cache name="helloworld1"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="true"
memoryStoreEvictionPolicy="LRU"/>
<!-- helloworld2缓存 -->
<cache name="helloworld2"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
<!--
以下属性是必须的:
name: Cache的名称,必须是唯一的(ehcache会把这个cache放到HashMap里)。
iskStore : 指定数据存储位置,可指定磁盘中的文件夹位置
defaultCache : 默认的管理策略
maxElementsInMemory: 在内存中缓存的element的最大数目。
maxElementsOnDisk: 在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
eternal: 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上。
以下属性是可选的:
timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问。
timeToLiveSeconds: 对象存活时间,指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问。
diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
diskExpiryThreadIntervalSeconds: 对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。
diskSpoolBufferSizeMB: DiskStore使用的磁盘大小,默认值30MB。每个cache使用各自的DiskStore。
memoryStoreEvictionPolicy: 如果内存中数据超过内存限制,向磁盘缓存时的策略。默认值LRU,可选FIFO、LFU。
缓存的3 种清空策略 :
FIFO ,first in first out (先进先出).
LFU , Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存。
LRU ,Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
4.编写测试代码查看使用效果
@Test
void ehcacheTest() {
//通过读取ehcache配置文件来创建缓存管理器即CacheManager
InputStream in = CacheDemoApplicationTests.class.getClassLoader().getResourceAsStream("ehcache.xml");
CacheManager cacheManager = CacheManager.create(in);
// 创建一个缓存实例(在配置文件中获取一个缓存实例)
final Cache cache = cacheManager.getCache("helloworld1");
final String key = "greeting";
final String key1 = "greeting1";
//创建一个数据容器来存放我们所创建的element
final Element putGreeting = new Element(key, "Hello, World!");
final Element putGreeting1 = new Element(key1, "Hello Ehcache");
//将数据放入到缓存实例中
cache.put(putGreeting);
cache.put(putGreeting1);
//取值
final Cache cache2 = cacheManager.getCache("helloworld1");
final Element getGreeting = cache2.get(key);
final Element getGreeting1 = cache2.get(key1);
// Print the value
System.out.println("value======//========"+getGreeting.getObjectValue());
System.out.println("value1=====//========"+getGreeting1.getObjectKey());
}
5.分析结果
可以从控制台中发现我们成功取到缓存值!
实现原理
Ehcache 的实现原理非常复杂,涉及多个层级的组件和逻辑,但我会尝试基于其核心组成部分和关键流程给出一个简化的概述,并尽可能结合源码细节进行解释。请注意,由于源码细节可能随版本更新而变化,以下内容基于Ehcache的一般设计思路和常见版本特性。
核心组件
- CacheManager:管理一组缓存实例的工厂类,负责创建、获取、销毁缓存。在Ehcache中,
CacheManager
是一个单例对象,可以通过CacheManager.getInstance()
获取。 - Cache:代表一个具体的缓存,包含了一系列配置项(如最大内存大小、是否溢写到磁盘等)和数据存储结构。每个
Cache
都有一个唯一的名称,并且内部维护了缓存项的集合。 - Element:缓存的基本单位,包含一个key-value对以及一些额外的元数据(如过期时间、创建时间等)。
- Store:数据存储层,包括内存存储和磁盘存储。内存存储通常是基于
SelfPopulatingCache
(自生式缓存)和MemoryStore
实现,而磁盘存储则通过DiskStore
实现。
核心流程
- 缓存初始化:当通过
CacheManager
创建或获取一个Cache
时,首先会检查是否存在配置文件中定义的相应缓存。如果存在,则根据配置创建Cache
实例。这个过程会初始化存储结构,比如创建内存存储和配置磁盘存储策略。
// 简化版示例代码,非实际源码
private static CacheManager cacheManager;
static {
cacheManagerInit();
}
/**
* EhcacheConstants.CACHE_NAME, cache name
* EhcacheConstants.MAX_ELEMENTS_MEMORY, 缓存最大个数
* EhcacheConstants.WHETHER_OVERFLOW_TODISK, 内存不足时是否启用磁盘缓存
* EhcacheConstants.WHETHER_ETERNAL, 缓存中的对象是否为永久的,如果是,超过设置将被忽略,对象从不过期
* EhcacheConstants.timeToLiveSeconds, 缓存数据的生存时间;元素从构建到消亡的最大时间间隔值,只在元素不是永久保存时生效;若该值为0表示该元素可以停顿无穷长的时间
* EhcacheConstants.timeToIdleSeconds 对象在失效前的允许闲置时间,仅当eternal=false对象不是永久有效时使用;可选属性,默认值是0可闲置时间无穷大;
*/
private static CacheManager cacheManagerInit() {
if (cacheManager == null) {
//创建一个缓存管理器
cacheManager = CacheManager.create();
//建立一个缓存实例
Cache memoryOnlyCache = new Cache(EhcacheConstants.CACHE_NAME,
EhcacheConstants.MAX_ELEMENTS_MEMORY,
EhcacheConstants.WHETHER_OVERFLOW_TODISK,
EhcacheConstants.WHETHER_ETERNAL,
EhcacheConstants.timeToLiveSeconds,
EhcacheConstants.timeToIdleSeconds);
//在内存管理器中添加缓存实例
cacheManager.addCache(memoryOnlyCache);
return cacheManager;
}
return cacheManager;
}
- 缓存读取:当通过key访问缓存时,Ehcache首先在内存中查找,如果找到了对应的
Element
,则直接返回其value。如果内存中没有找到,且配置了磁盘存储和溢写策略,则尝试从磁盘加载数据。
public static Object getValue(String ehcacheInstanceName, Object key) {
Cache cache = cacheManager.getCache(ehcacheInstanceName);
Object value = cache.get(key).getObjectValue();
return value;
}
逻辑解释
2.1 初始化观测器并检查状态:
this.getObserver.begin();
开启获取操作的观测记录。this.checkStatus();
检查缓存当前状态是否允许执行get操作。
2.2 处理禁用状态:
- 如果缓存被标记为
disabled
,则记录观测结果为GetOutcome.MISS_NOT_FOUND
(未找到),并直接返回null,表示获取失败。
2.3 尝试从缓存中获取元素:
Element element = this.compoundStore.get(key);
试图从复合存储器(compoundStore
)中根据提供的键获取元素。
2.4 处理未找到的情况:
- 如果元素为null,说明缓存中没有对应键的值,观测记录为
GetOutcome.MISS_NOT_FOUND
,然后返回null。
2.5 检查元素是否过期:
if (this.isExpired(element)) { ... }
判断获取到的元素是否已经过期。如果过期,执行立即删除操作,并记录观测结果为GetOutcome.MISS_EXPIRED
(过期未命中),然后返回null。
2.6 更新访问统计和返回元素:
- 对于未过期的元素,如果不需要跳过访问统计更新(通过
skipUpdateAccessStatistics
判断),则调用element.updateAccessStatistics();
更新元素的访问统计信息。 - 记录观测结果为
GetOutcome.HIT
(命中),最后返回找到的元素。
- 缓存写入:写入操作首先是写入内存,然后根据配置可能还会写入磁盘。Ehcache会检查当前内存使用情况,如果超过配置的最大内存限制,可能会触发溢写到磁盘的逻辑。
public static void put(String ehcacheInstanceName, String key, Object value) {
Cache