目录
摘要
1. Java缓存技术概述
1.1定义
1.2 优势
1.3 应用场景
2. Java中的内置缓存实现
2.1 通过通过HashMap和ConcurrentHashMap实现缓存
3. Java缓存框架
3.1 Redis
3.1.1 redis的简介
3.1.4 Redis的工作原理
3.1.5 总结
3.2 Ehcache
3.2.1 Ehcache的简介
3.2.2 Ehcache的简单使用
3.2.3 数据的持久化
3.2.4 总结
3.3 Caffeine
3.3.1 Caffeine的简介
3.3.2 Caffeine的简单使用
3.3.3 caffeine总结
4. 报告总结
缓存,作为提升系统性能的关键策略,在Java编程环境中扮演着重要角色。本报告聚焦于Java缓存技术,深入探讨了其基本概念、应用场景及多样化实现。
报告首先明确了缓存的定义,并强调了其在读操作频繁、数据计算复杂及数据更新不频繁等场景下的显著优势。在Java中,缓存的实现方式多样,包括利用本地数据结构实现的本地缓存、借助Java标准库提供的缓存功能。
此外,报告还介绍了几个广受欢迎的Java缓存框架,如Redis(高性能分布式内存缓存)、Ehcache(开源Java缓存框架,支持分布式缓存)以及Caffeine(高性能Java内存缓存库,专注于快速响应和高效内存利用)。这些框架各具特色,为开发者提供了丰富的选择空间。
通过本报告的阐述,读者将全面理解Java缓存技术的核心概念、应用场景及实现方式,为优化系统性能提供有力的理论与实践支持。
缓存是数据访问的加速器,它为数据提供了一个快速的临时栖息地,以减少数据检索的时间消耗。在Java编程中,缓存技术是提升数据处理速度和系统性能的关键工具。它利用内存这一高速存储介质,保存数据的副本,以便快速访问,避免了对较慢存储设备(如硬盘)的频繁访问。
对于需要频繁访问相同数据的应用,Java缓存技术显得尤为重要。它像一座桥梁,连接了对快速数据的需求和较慢的数据存储,减少了对数据源的直接访问,提高了数据处理的效率。
简而言之,Java中的缓存技术是一种高效的数据处理策略,它利用内存的高速访问特性,为应用程序提供了快速的数据检索服务,从而提高了系统的响应速度和用户体验。这种技术不仅减轻了系统的资源负担,还为数据的快速处理和有效利用提供了新的途径。
1. 提高响应速度:通过在快速的存储介质中保存数据副本,缓存减少了数据检索时间,从而加快了应用程序的响应速度。
2. 减轻后端负载:缓存减少了对数据库或其他数据源的访问次数,从而减轻了后端系统的负担。
3. 提升用户体验:更快的数据访问速度和更流畅的交互显著提升了用户的体验。
4. 降低成本:缓存减少了对昂贵资源(如数据库查询)的依赖,有助于降低运营成本。
1. 数据库缓存:
- 在高并发访问的系统中,数据库压力可能非常大。为了缓解数据库压力,可以使用缓存来存储常用的查询结果。当再次访问这些数据时,可以直接从缓存中读取,而无需查询数据库,从而显著提高系统的响应速度。
- 缓存还可以用于存储数据库中的临时数据,如会话信息、用户登录状态等,以避免频繁访问数据库。
- Web应用缓存:
- 在Web应用中,缓存可以用于存储静态资源(如图片、CSS、JavaScript等)和动态内容(如网页、API响应等)。
- 通过缓存静态资源,可以减少对服务器的请求次数,降低服务器负载,提高网页加载速度。
- 对于动态内容,可以使用缓存来存储重复的查询结果或计算结果,以减少数据库查询和计算的时间。
- 分布式系统缓存:
- 在分布式系统中,缓存可以用于实现分布式锁、分布式会话共享等功能。
- 通过使用分布式锁,可以确保多个进程或线程在访问共享资源时的同步性。
- 分布式会话共享则允许多个服务器共享用户的会话信息,从而提供一致的用户体验。
4. CDN缓存:
- 内容分发网络(CDN)中的缓存用于存储和分发静态内容(如图片、视频、音频等)。
- 通过将内容缓存在CDN节点上,可以缩短用户访问内容的距离和时间,提高内容的加载速度和可用性。
5. 应用层缓存:
- 在应用层,缓存可以用于存储应用程序的临时数据、计算结果或中间状态。
- 这有助于减少应用程序对后端服务的请求次数,提高应用程序的响应速度和性能。
- 硬件缓存:
- 在计算机硬件中,缓存(如CPU缓存、硬盘缓存等)用于加速数据的访问速度。
- 通过将常用数据存储在离处理器更近的缓存中,可以减少对慢速存储设备的访问次数,提高系统的整体性能。
- Java中的内置缓存实现
2.1 通过通过HashMap和ConcurrentHashMap实现缓存
Java 提供了多种基础数据结构,其中 HashMap 和 ConcurrentHashMap 特别适合用于构建内存缓存。HashMap 是一个高效的哈希表实现,而 ConcurrentHashMap 则在此基础上进一步优化,专为多线程环境设计,能够提供卓越的并发访问性能。
然而,这些数据结构有一个共同的局限性:它们不支持数据持久化。因此,当应用程序重启时,所有存储在其中的缓存数据都会丢失。
在特定场景下,如果需要对某些操作进行更精细的控制,以确保其原子性,那么 ConcurrentHashMap 可能无法完全满足需求。此时,开发者可能需要考虑使用更复杂的原子操作,或者将 ConcurrentHashMap 与其他并发控制工具(如锁机制)结合使用,以实现所需的数据一致性和完整性。
以下是简单的代码实现:
public class ConcurrentMyCache {private Map<String, Object> cache = new ConcurrentHashMap<>();public void put(String key, Object value) {cache.put(key, value);}public Object get(String key) {return cache.get(key);}public void remove(String key) {cache.remove(key);}public void clear() {cache.clear();}public int size() {return cache.size();}
}public class MyCache {private Map<String, Object> cache = new HashMap<>();// 向缓存中放入键值对public void put(String key, Object value) {cache.put(key, value);}// 从缓存中获取值public Object get(String key) {return cache.get(key);}// 从缓存中移除键值对public void remove(String key) {cache.remove(key);}// 清空缓存public void clear() {cache.clear();}// 获取缓存大小public int size() {return cache.size();}}@SpringBootTestclass CacheTest {@Testvoid testCache(){ConcurrentMyCache cache=new ConcurrentMyCache();cache.put("key1","zhangsan");cache.put("key2","lisi");System.out.println("key1: " + cache.get("key1"));System.out.println("key2: " + cache.get("key2"));MyCache cache1=new MyCache();cache1.put("key3","wangwu");cache1.put("key4","lht");System.out.println("key3: " + cache1.get("key3"));System.out.println("key4: " + cache1.get("key4"));}}
- Java缓存框架
Redis是一个完全开源免费的高性能(NOSQL)的key-value数据库。它遵守BSD协议,使用ANSI C语言编写,并支持网络和持久化。Redis拥有极高的性能,每秒可以进行11万次的读取操作和8.1万次的写入操作。它支持丰富的数据类型,包括String、Hash、List、Set和Ordered Set,并且所有的操作都是原子性的。此外,Redis还提供了多种特性,如发布/订阅、通知、key过期等。Redis采用自己实现的分离器来实现高速的读写操作,效率非常高。Redis是一个简单、高效、分布式、基于内存的缓存工具,通过网络连接提供Key-Value式的缓存服务。
Redis可以通过配置文件设置密码参数,这样客户端连接到Redis服务就需要密码验证,从而提高Redis服务的安全性。
3.1.2 redis的简单使用
@Datapublic class User implements Serializable {private Integer id;private String username;private String password;}
通过自动装配redis里面的RedisTemplate这个类里面的相关配置并且封装方法方便后期使用。
/*** spring redis 工具类***/@SuppressWarnings(value = { "unchecked", "rawtypes" })@Componentpublic class RedisCache{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}}
3.1.3 redis的主要特征
- 键值(key-value)型,value支持多种不同数据结构,功能丰富
- 单线程,每个命令具备原子性
- 低延迟,速度快(基于内存、IO多路复用、良好的编码)。
- 支持数据持久化
- 支持主从集群、分片集群,
- 支持多语言客户端
1. 内存存储:数据完全存储在内存中,提供快速的读写访问,时间复杂度接近O(1)。
- 单线程架构:采用单线程处理请求,避免了多线程带来的上下文切换和锁竞争,简化了并发控制,提高了性能。
- 非阻塞IO:使用多路复用IO模型,能够非阻塞地处理多个客户端请求,提高了并发处理能力。
- Lua脚本执行:支持在Lua脚本中执行命令,允许用户执行复杂的逻辑和操作,增加了操作的灵活性。
Redis是一种高效的内存键值存储系统,广泛用于缓存管理、会话保持及实时数据处理等多种场景。它的优势体现在:极快的读写速度、对数据结构的多样化支持、具备持久化功能(包括RDB快照和AOF日志)、支持分布式部署,以及提供强大的原子操作特性。
EhCache是一个高效的纯Java进程内缓存框架,支持单机和分布式缓存,适用于需要快速数据访问的场景。它具备简单易用、快速访问、多种缓存策略(如堆缓存、磁盘缓存、集群缓存)等优点。EhCache的缓存数据有两级,一级是内存,二级是磁盘,当内存不足时,数据可以自动溢出到磁盘,从而解决了容量问题。此外,EhCache还支持缓存数据在虚拟机重启时写入磁盘,以及通过RMI、可插入API等方式进行分布式缓存。
在Spring Boot中,EhCache可以通过配置文件和Bean注入来使用,提供了灵活的缓存策略配置,如缓存对象的最大数量、对象是否永不过期、空闲时间和存活时间等。同时,EhCache还提供了缓存和缓存管理器的侦听接口,支持多缓存管理器实例以及一个实例的多个缓存区域。
虽然EhCache在非集群环境下可能导致敏感数据更新延迟,但它非常适合高QPS场景和小量数据缓存需求。使用EhCache时,建议设置较短的过期时间以保证数据的及时更新。
首先,需要初始化一个缓存管理器(CacheManager),利用它来创建新的缓存或者访问已有的缓存。之后,可以在这些缓存中存储数据,或者从缓存中检索数据。
@Testpublic void test() {// 初始化 CacheManagerCacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()// 一个CacheManager可以管理多个Cache.withCache("ehcacheDemo",CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,// heap相当于设置数据在堆内存中存储的 个数 或者 大小ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB).build()).build()).build(true);// 如果 CacheManagerBuilder.build(); 如果没有传参数,需要手动调用init()// cacheManager.init();// 基于 CacheManager 获取 Cache对象Cache<String, String> ehCache = cacheManager.getCache("ehcacheDemo", String.class, String.class);// 放去缓存ehCache.put("ehcache", "hello ehcache");// 取System.out.println(ehCache.get("ehcache"));}
EhCache 提供了非常灵活和强大的配置选项,这使得它能够适应各种不同的缓存需求。
<config xmlns="http://www.ehcache.org/v3"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd"><!--定义缓存--><cache alias="squaredNumber"uses-template="myTemplate"><!--缓存使用的缓存模板的名称--><key-type>java.lang.Integer</key-type><value-type>java.lang.Integer</value-type><heap unit="entries">10</heap></cache><!--定义缓存模板--><cache-template name="myTemplate"><expiry><ttl unit="seconds">60</ttl><!--缓存项的过期策略,60秒过期--></expiry></cache-template>
</config>
Ehcache还可以将数据落地本地磁盘,这样的话,当服务重启后,依然会从磁盘反序列化数据到内存中,实现数据的持久化代码如下:
@Test
public void test1() {// 声明存储位置String path = "D:\\ehcache";// 初始化 CacheManagerCacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()// 设置存储位置.with(CacheManagerBuilder.persistence(path))// 一个CacheManager可以管理多个Cache.withCache("ehcacheDemo",CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,// heap相当于设置数据在堆内存中存储的 个数 或者 大小ResourcePoolsBuilder.newResourcePoolsBuilder()// 堆内内存.heap(10, MemoryUnit.MB)// 堆外内存// off-heap大小必须 大于 heap 设置的内存大小.offheap(15,MemoryUnit.MB)// 磁盘存储,记得添加true,才能正常持久化,并且序列号以及反序列化// disk大小必须 大于 off-heap 设置的内存.disk(20,MemoryUnit.MB,true)).build()).build(true);// 如果 CacheManagerBuilder.build(); 如果没有传参数,需要手动调用init()// cacheManager.init();// 基于 CacheManager 获取 Cache对象Cache<String, String> ehCache = cacheManager.getCache("ehcacheDemo", String.class, String.class);// 放去缓存ehCache.put("ehcache", "hello ehcache");// 取System.out.println(ehCache.get("ehcache"));// 保证数据正常持久化不丢失,记得 close()cacheManager.close();
}
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点。EhCache支持单机缓存和分布式缓存,分布式可以理解为缓存数据的共享,这就导致内存缓存数据量偏小。ehcache缓存非常存储和读取非常快。
Caffeine是一个高性能的Java缓存库,它通过精细的数据结构和高效的内存管理,确保了在高并发环境下的快速访问。它提供了丰富的配置选项,包括缓存的最大容量、数据失效策略和自动刷新等,以满足不同应用程序的需求。Caffeine支持同步和异步两种数据加载方式,同步加载会阻塞主线程直到数据加载完成,而异步加载则允许主线程在数据加载时继续执行,从而提高系统的并发性。此外,Caffeine还支持多种过期策略,如基于时间或访问频率,以及注册监听器来监控缓存的变更事件,为应用程序提供了更细粒度的控制和监视。总的来说,Caffeine是一个功能强大、易于使用的缓存解决方案,非常适合需要高性能缓存的Java应用程序。
- 缓存加载策略
- Cache手动创建
最普通的一种缓存,无需指定加载方式,需要手动调用put()进行加载。需要注意的是put()方法对于已存在的key将进行覆盖,这点和Map的表现是一致的。在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。调用invalidate()方法,将手动移除缓存。
在多线程情况下,当使用get(key, k -> value)时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。
@Testpublic void test1() {Cache<Object, Object> cache = Caffeine.newBuilder()//初始数量.initialCapacity(10)//最大条数.maximumSize(10)//expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准//最后一次写操作后经过指定时间过期.expireAfterWrite(1, TimeUnit.SECONDS)//最后一次读或写操作后经过指定时间过期.expireAfterAccess(1, TimeUnit.SECONDS)//监听缓存被移除.removalListener((key, val, removalCause) -> { })//记录命中.recordStats().build();cache.put("1","张三");//张三System.out.println(cache.getIfPresent("1"));//存储的是默认值System.out.println(cache.get("2",o -> "默认值"));}
- Loading Cache自动创建
LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于,当缓存不存在/缓存已过期时,若调用get()方法,则会自动调用CacheLoader.load()方法加载最新值。调用getAll()方法将遍历所有的key调用get(),除非实现了CacheLoader.loadAll()方法。使用LoadingCache时,需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。
在多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新缓存完成。
//Loading Cache自动创建@Testpublic void test2() {LoadingCache<String, String> loadingCache = Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(10, TimeUnit.SECONDS).expireAfterWrite(10, TimeUnit.SECONDS).expireAfterAccess(10, TimeUnit.SECONDS).maximumSize(10)//根据key查询数据库里面的值,这里是个lamba表达式.build(key -> new Date().toString());loadingCache.put("1","张三");//张三System.out.println(loadingCache.getIfPresent("1"));//存储的是默认值System.out.println(loadingCache.get("2",o -> "默认值"));}
- Async Cache异步获取
AsyncCache是Cache的一个变体,其响应结果均为CompletableFuture,通过这种方式,AsyncCache对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.executor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力,它将以Cache进行返回。
在多线程情况下,当两个线程同时调用get(key, k -> value),则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞。
//Async Cache异步获取@Testpublic void test3() {AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10).buildAsync(key -> {try {// 模拟数据库查询延迟Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}// 模拟从数据库获取的数据return new Date().toString();});// 获取缓存中的值CompletableFuture<String> future = asyncLoadingCache.get("1");// 当获取完成时,打印结果future.thenAccept(System.out::println);// 等待一段时间,确保异步加载完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}
- 驱逐策略
驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐和基于时间的驱逐。基于容量的驱逐需要指定缓存容量的最大值,当缓存容量达到最大时,Caffeine将使用LRU策略对缓存进行淘汰;基于时间的驱逐策略如字面意思,可以设置在最后访问/写入一个缓存经过指定时间后,自动进行淘汰。
- 基于容量的驱逐(LRU)
基于容量的驱逐是指缓存在达到一定的容量后,会按照最近最少使用(Least Recently Used)的策略自动淘汰掉一些缓存项。这通常用于限制缓存占用的内存空间。
//基于容量驱逐@Testpublic void maximumSizeTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder()//超过10个后会使用W-TinyLFU算法进行淘汰.maximumSize(10).evictionListener((key, val, removalCause) -> {System.out.println("淘汰缓存:key:" + key + " val:" + val);}).build();for (int i = 1; i < 20; i++) {cache.put(i, i);}Thread.sleep(500);//缓存淘汰是异步的// 打印还没被淘汰的缓存System.out.println(cache.asMap());}
- 基于时间的驱逐
基于时间的驱逐是指缓存项在一定时间后自动被淘汰。可以基于最后一次写入时间或者最后一次访问时间来淘汰缓存项。
/*** 访问后到期(每次访问都会重置时间,也就是说如果一直被访问就不会被淘汰)*/@Testpublic void expireAfterAccessTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS)//可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护//若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除.scheduler(Scheduler.systemScheduler()).evictionListener((key, val, removalCause) -> {log.info("淘汰缓存:key:{} val:{}", key, val);}).build();cache.put(1, 2);System.out.println(cache.getIfPresent(1));Thread.sleep(3000);System.out.println(cache.getIfPresent(1));//null
- 刷新机制
Caffeine 缓存库提供了灵活的刷新机制,可以在缓存项即将过期时自动刷新数据,以确保缓存数据的时效性。refreshAfterWrite()表示x秒后自动刷新缓存的策略可以配合淘汰策略使用,注意的是刷新机制只支持LoadingCache和AsyncLoadingCache。
//刷新机制private static int NUM = 0;@Testpublic void refreshAfterWriteTest() throws InterruptedException {LoadingCache<Integer, Integer> cache = Caffeine.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS)//模拟获取数据,每次获取就自增1.build(integer -> ++NUM);//获取ID=1的值,由于缓存里还没有,所以会自动放入缓存System.out.println(cache.get(1));// 1// 延迟2秒后,理论上自动刷新缓存后取到的值是2// 但其实不是,值还是1,因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新// 而是x秒后&&第二次调用getIfPresent的时候才会被动刷新Thread.sleep(2000);System.out.println(cache.getIfPresent(1));// 1//此时才会刷新缓存,而第一次拿到的还是旧值System.out.println(cache.getIfPresent(1));// 2}
- 统计
Caffeine 缓存库提供了内置的统计功能,可以帮助开发者监控和调优缓存性能。通过启用统计功能,你可以收集关于缓存操作的详细信息,例如命中率、未命中率、加载次数、加载时间等。
//统计@Testpublic void requestCount(){LoadingCache<String, String> cache = Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10)//开启记录缓存命中率等信息.recordStats()//根据key查询数据库里面的值.build(key -> {Thread.sleep(1000);return new Date().toString();});cache.put("1", "shawn");cache.get("1");/** hitCount :命中的次数* missCount:未命中次数* requestCount:请求次数* hitRate:命中率* missRate:丢失率* loadSuccessCount:成功加载新值的次数* loadExceptionCount:失败加载新值的次数* totalLoadCount:总条数* loadExceptionRate:失败加载新值的比率* totalLoadTime:全部加载时间* evictionCount:丢失的条数*/System.out.println(cache.stats());
Caffeine是一个高性能的Java缓存库,它提供了丰富的配置选项和强大的缓存策略,包括最近最少使用(LRU)、最近最不常用(LFU)、先进先出(FIFO)等。它支持自动刷新、定时失效、大小限制和异步加载,确保了数据的时效性和缓存的高效性。Caffeine还内置了统计监控功能,帮助开发者了解缓存性能并进行调优。其线程安全、易于集成和使用,是提升Java应用性能的理想选择。
本报告深入探讨了Java缓存技术,包括其定义、优势、应用场景及实现方式。缓存通过在内存中存储数据副本,减少了对慢速存储设备的访问,从而加速了数据检索,提高了系统性能。Java提供了多种缓存实现,如利用HashMap和ConcurrentHashMap构建本地缓存,以及使用Redis、Ehcache和Caffeine等缓存框架。这些框架支持不同的缓存策略,如堆缓存、磁盘缓存、集群缓存,以及自动刷新和过期策略。Caffeine特别受到关注,它是一个高性能的Java缓存库,提供了丰富的配置选项,包括定时失效、大小限制、同步和异步加载,以及多种过期策略。Caffeine还内置了统计监控功能,帮助开发者优化缓存性能。报告通过介绍这些技术和框架,为读者提供了理论与实践相结合的缓存解决方案,以优化系统性能。