缓存(Cache)是一种存储技术,用于临时存放从原始数据源(如硬盘、数据库或网络)获取的数据副本,目的是加快数据的访问速度,减少不必要的重复处理,进而提升系统整体的性能和响应效率。它是计算机科学中“空间换时间”策略的一个典型应用,即通过牺牲少量的存储空间来换取数据访问速度的显著提升。本文通过对Java缓存技术的调研,总结了四种主流的Java缓存技术,并结合实际使用情况,分析了各自的优缺点,旨在为Java开发者提供缓存技术选型的参考。
Introduction:随着互联网技术的飞速发展,数据量呈现出爆炸式增长,对系统性能提出了更高的要求。缓存技术作为一种优化手段,越来越受到开发者的重视。在Java领域,有许多优秀的缓存技术,它们各自具有不同的特点和适用场景。
缓存技术可以用一句话概况:更快读写的存储介质+减少IO+减少CPU计算=性能优化。[1]
缓存技术工作主要有以下三个方面进行:
1.数据存储:系统初次发起数据请求时,数据会从原始数据源进行加载,并同步至缓存区域。这个过程一般发生在首次数据访问或数据更新导致的重新加载数据环节。
2.快速访问:之后,当系统再次请求相同的数据时,可以直接从缓存中获取,而无需再次访问较慢的数据源。由于缓存通常位于更快的存储介质上(如RAM),数据访问速度远高于直接从硬盘或网络获取。
3.数据更新与同步:缓存中的数据并不是永久不变的,需要有机制来维护其与数据源之间的一致性。常见的策略包括定时刷新、写穿(write-through)、写回(write-back)等。此外,为了高效利用有限的缓存空间,还会有算法(如最近最少使用LRU、最不经常使用LFU等)定期淘汰旧数据,为新数据腾出空间。
比如,拿“CPU缓存”来举例,当 CPU 要读取一个数据时,首先从 CPU 缓存中查找,找到就立即读取并送给 CPU 处理;没有找到,就从速率相对较慢的内存中读取并送给 CPU 处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。[2]
对于存储介质,可以将存储介质分为以下类型(如图1):
越往上,存储器的容量越小、成本越高、速度越快。由于 CPU 和主存之间巨大的速度差异,系统设计者被迫在 CPU 寄存器和主存之间插入了一个小的 SRAM 高速缓存存储器称为 L1 缓存,大约可以在 2-4 个时钟周期(计算机中最小的时间单位)内访问。再后来发现 L1 高速缓存和主存之间还是有较大差距,又在 L1 高速缓存和主存之间插入了 L2 缓存,大约可以在 10 个时钟周期内访问。后面还新增了 L3 等,于是,在这样的模式下,在不断的演变中形成了现在的存储体系。[2]
缓存按照技术层次分类可以分为硬件缓存和软件缓存,本文主要接触的是软件缓存里面的数据库缓存。[1][3]
2.1.1 硬件缓存
CPU缓存:该缓存位于中央处理单元与主存储器之间,其作用是提升数据与指令的读取效率,从而缩短CPU的空闲等待周期。
GPU缓存:与CPU缓存相似,这种缓存是专门为图形处理单元定制的,旨在加快图形渲染及数据处理的效率。
2.1.2 软件缓存
操作系统缓存:例如文件系统缓存,它用于提升文件输入输出的处理速度。
数据库缓存:数据库管理系统内置的缓存机制。
浏览器缓存:这种缓存用于存储用户已浏览的网页资源,包括图片、CSS样式表和JavaScript脚本等,以此加快网页的加载速度。
代理服务器缓存:位于用户客户端与原始服务器之间的缓存,它保存频繁访问的网页内容,以此来降低网络带宽的使用并缩短服务响应的时间。
Java缓存技术主要分为本地缓存和分布式缓存。本地缓存指的是在单个应用内部实现的缓存,如Ehcache、Guava Cache等。分布式缓存则是在多个应用之间共享缓存数据,如Redis、Memcached等。本文主要实现以及研究的缓存技术有:Redis缓存,Spring chache缓存,MyBatis缓存,EhCache缓存。
Redis是平常开发中最常用到的缓存中间件,Redis是一个开源的、高性能的键值存储系统,它以高效的数据结构存储数据,并支持多种数据类型,包括字符串(Strings)、哈希(Hashes)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)等。由于它的超高性能,使其成为我们在开发中首选的缓存工具。其实在八股文的时候对于Redis缓存也有所了解。
为了提高网站响应速度,企业会将热点数据保存在内存中而不是直接从后端数据库中读取。大型网站应用,热点数据往往巨大,几十G上百G是很正常的事,这种情况下,就需要用到缓存服务器,通过缓存服务器承载大部分用户请求,小部分用户请求交给后端服务器处理,如此一来,就可以大大提高用户访问的速度,提升用户使用体验。[4]
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
可能下载的时间比较久需要耐心等待
实现RedisConfig类之后记得需要在启动类之前加入@EnableCaching标签
其实对于redis加入查询代码中,原理就是通过构建键值对,如果查询相应的键值对的时候能找到以前查找过的,那就将他进行返回;否则,找到对应的数据库中的数据,然后将数据库中的数据插入redis,返回。
这里我们以我宠物商店的查询功能为例(如图2):
实现代码如下:
@Resource
private RedisTemplate redisTemplate;
@PostMapping("search")
@ResponseBody
public CommonResponse<List<Product>> searchProduct(@RequestParam("keyword") String keyword){
System.out.println(keyword);
CommonResponse<List<Product>> productList = null;
// 动态构造key
String key = "score_note_" + keyword; // 例:score_note_fish
productList = (CommonResponse<List<Product>>) redisTemplate.opsForValue().get(key);
if (productList != null) {
// 如果存在, 直接返回, 无需查询数据库
System.out.println("通过redis查询");
return productList;
}
productList = catalogService.searchProductList(keyword);
// 将查询到的数据缓存到Redis中
redisTemplate.opsForValue().set(key, productList, 1, TimeUnit.HOURS);
return catalogService.searchProductList(keyword);
}
在首次查询的时候后端显示只会输出一个关键词“fish”
第二次查询的时候会出现“通过redis查询”的字样:
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;它为开发者提供了一套基于注解的缓存抽象。这一抽象层使得开发者能够在不修改太多代码的情况下,方便地将缓存逻辑集成到应用程序中,从而提升应用的性能和响应速度。Spring Cache 的设计目标是简化缓存的使用,同时保持足够的灵活性以支持多种缓存实现。[5]
实现如下:
1.注入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.添加@Cacheable注释:
@Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
此处的value是必需的,它指定了你的缓存存放在哪块命名空间。
在注释掉redis缓存的代码之后,我们对数据库查询方法加入上述@Cacheable标签,如下所示:
并且为了保证实验的准确性,我们这次使用“dog”等多个关键词进行搜索:
可以发现,在使用了多个关键词进行搜索的情况下,只有在运行第一次的时候会运行函数体,而在后面的运行中,因为Spring cache的存在,他不会使用函数体,而是直接在cache中找对应的是否有相关数据。
但是,其实对于Spring Cache我们不止有这种普通的操作,当我们需要缓存的地方越来越多,我们可以使用@CacheConfig(cacheNames = {"myCache"})注解来统一指定value的值,这时可省略value,如果在我们的方法依旧写上了value,那么依然以方法的value值为准。
也不仅仅有cacheable标签@CachePut注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。
MyBatis 是一个流行的 Java 数据库访问层框架,它提供了一级和二级缓存机制来优化数据库的查询性能。缓存的主要作用是减少对数据库的访问次数,将查询结果存储在内存中,从而提高应用的性能。[1][6]
一级缓存是基于 SqlSession 级别的,它默认是开启的。在同一个 SqlSession 中,如果两次执行相同的 SQL 语句,第一次执行后,MyBatis 会将查询结果存储在一个内部的数据结构中(通常是一个 HashMap)。当第二次查询相同的数据时,MyBatis 会直接从这个内部结构中获取数据,而不是再次查询数据库。
二级缓存则是为了延长查询结果的保存时间,从而提高系统性能。它也可以用于共享数据。二级缓存是基于 namespace 级别的,它可以被多个 SqlSession 共享。默认情况下,MyBatis 的二级缓存是关闭的,需要在全局配置文件中开启。二级缓存需要在 mapper.xml 文件中添加 <cache/> 标签来启用。
1.编写测试代码
@Autowired
private CatalogService catalogService;
/**
* 批量查询缓存
* 缓存:将数据临时存储在(本地硬盘,内存),减少对数据的访问
*
* 一级缓存是SqlSession级别的,MyBatis默认开启
* 在同一个SqlSession中可以将第一次查询的数据缓存到SqlSession
* 第二次查询相同数据时,就可以直接从SqlSession获取
*/
@Test
public void test5() {
//第一次查询
Object data = catalogService.searchProductList("fish").getData();
List<Product> productList = (List<Product>) data;
for(Product l:productList)
{
System.out.println(l.getName());
}
//第二次查询
Object data1 = catalogService.searchProductList("fish").getData();
List<Product> productList1 = (List<Product>) data1;
for(Product l:productList1)
{
System.out.println(l.getName());
}
}
这里我们查询依旧查询fish,在不打开二级缓存的情况下,最终输出结果如下所示(这里注意,为了测试的准确性,我们需要将前面的spring cache代码删除):
如上图可以看到,在不开启二级缓存模式的情况下,会调用该函数两次,进行两次查询工作。
2.开启二级缓存模式
在application.yml或application.properties中开启缓存:
在需要使用二级缓存的Mapper接口上添加@CacheNamespace注解。
加入注解之后,我们就可以开始二次测试:
可以看到虽然这里是有两次出现“有进行查询工作”,但是有字段显示是从cache中进行的查询,这里可以看出来,mabatis本身自带的缓存功能,是处于更细致的缓存,对于每个mapper进行的缓存。比如这里我们的productmapper进行的缓存技术,虽然也是实现缓存功能,但是和上面不同的是对于第二次查询会运行到这里。可以看出MyBatis的二级缓存是建立在SqlSessionFactory级别上的,旨在提高应用程序的整体性能,通过跨SqlSession重用已经执行过的查询结果。
EhCache是一个Java缓存框架,提供快速、简单的内存和分布式缓存解决方案。配置包括设置磁盘存储、默认缓存及自定义缓存策略。在SpringBoot中,通过配置文件和Bean注入来使用EhCache。缓存管理器支持监听接口和多实例。定时任务可用来清除过期缓存。适用场景包括高QPS场景和小量缓存数据。但需要注意,非集群环境下敏感数据更新可能存在延迟。[9]
实现步骤如下:
1.添加xml配置文件:
2.运行有关测试类
测试后发现可以取出缓存值。
但是总体来说,EhCache实现还是比较复杂,但是看在后来在所有的Spring(java项目)中实现也和Spirng cache中类似有很多标签可以放在具体项目实现过程中:
@CacheConfig:声明在类上,指定本类上所有使用缓存的方法的缓存名称,如有注解@Cacheable的方法。[10]
@Cacheable:使用@Cacheable注解的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,如果存在则执行方法,并将返回结果存入指定的缓存中。
@CachePut:使用@CachePut注解的方法,Spring在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CacheEvict:清除缓存,属性allEntries为true则删除所有缓存,属性beforeInvocation表示缓存的清除是在方法前执行还是方法后执行,默认是为false,方法执行后删除。
@Caching:分组注解,可以包含多个@Cacheable和@CachePut,一般在复杂业务的情况下使用。
本文主要进行的是有关于缓存技术的分析,以及四种缓存技术的实现,这里也引发了一些对于缓存技术的思考。首先,对于我们软件缓存而言,其主要存在于内存与数据库之间,以提高数据库的运行速度。
在现有的互联网应用中,缓存的使用是一种能够提升服务快速响应的关键技术。[9]
实验主要实现了四种不同的缓存机制,其中,对于不同的Redis缓存机制有不同的处理办法,对于Redis缓存机制来说,他可以直接在业务控制代码中实现,放在业务控制代码中,如果以前查询过,就取出,没有则进入数据库,然后进行查询;对于SpringCache缓存机制,我认为,对于一个Spring自己的缓存机制,他的功能做的已经很好了,不想ehcache缓存机制,他是存在自己的回收机制,而ehcache需要编写查看元素是否过期;对于Mybatis缓存机制,他有两种实现,一级缓存和二级缓存,一级缓存是默认开启的,如果sql语句相同,那么在两次连续查询之后他会自动将上一次的进行输出,而对于二级缓存机制,我认为是相较于前面的缓存机制,更加深入贴近数据库,是相对于每一个mapper进行的实现。
其实在使用过程中,也能体会到各种缓存机制的优缺点,在不同的处理环境之下,也是需要根据不同情况选择缓存机制,比如,如果在业务代码比较精细的时候,需要跨多个服务器或应用共享数据的场景,并且对于性能要求较高,数据结构需求较多的时候,可以使用Redis。只需要简单的缓存实现的话,就可以选择Spring cache,通过注解的方式即可实现缓存功能,无需编写复杂的缓存逻辑。而EhCache则是纯Java的进程内缓存框架,适用于单机应用或不需要分布式缓存的环境。
References
- 缓存技术实战[一文讲透!](Redis、Ecache等常用缓存原理介绍及实战)_redis ecache-优快云博客
- 缓存技术原理_缓存原理-优快云博客
- 干货|java缓存技术详解 - Frank_Lei - 博客园
- Redis缓存的介绍与应用(从入门到精通以及四种模式的应用)_缓存服务产品介绍 缓存服务产品-redis-优快云博客
- 史上最全的Spring Boot Cache使用与整合_springbootcache-优快云博客
- MyBatis缓存看这一篇就够了(一级缓存+二级缓存+缓存失效+缓存配置+工作模式+测试)_直接修改数据库数据会使mybatis一级缓存和二级缓存的使用场景失效吗-优快云博客
- 分布式数据之缓存技术,这次我是真的搞懂了 - 知乎
- EhCache看这一篇就够了-优快云博客
- 缓存的万字总结,肝了! - 知乎
- Spring Boot整合ehcache的详细使用_springboot 整合 ecache 详细过程-优快云博客