Java缓存技术综合总结报告

发布时间 2024-01-02 11:28:23作者: lmcool-
摘要

​ 缓存是提高系统性能的重要手段之一,Java作为一种流行的编程语言,拥有丰富的缓存实现和框架。本报告将深入探讨Java中缓存的相关知识,包括缓存的定义、常见的缓存使用场景、Java中的缓存实现方式、以及一些流行的Java缓存框架。

​ 本报告总结了Java中缓存的关键概念、应用场景和多种实现方式。缓存作为提高系统性能的重要手段,在Java中有着多样化的实现选择。报告首先介绍了缓存的定义及其优势,强调了在读密集型、数据计算成本高昂以及数据不经常变化等场景下,使用缓存能够有效提高系统性能。随后,详细探讨了Java中的缓存实现方式,包括本地缓存、Java标准库支持和自定义缓存实现。最后,报告介绍了几个流行的Java缓存框架,如Redis、Ehcache、Guava Cache和Caffeine,以帮助开发者根据项目需求选择合适的工具。通过本报告,读者将对Java中缓存的概念、应用及相关框架有深入了解,为优化系统性能提供了有力的指导。

1、缓存概述

1.1 定义

​ 缓存是一种用于存储数据的临时存储区域,目的是在后续访问相同数据时,可以更快地获取数据。通常位于数据访问路径的中间,以减少对原始数据源的频繁访问。

​ 在Java中,缓存是一种用于存储临时数据的技术,目的是提高数据的访问速度和性能。缓存通常位于内存中,因为内存的访问速度比磁盘或其他存储介质快得多。缓存可以存储先前访问过的数据副本,以便在下一次请求时能够更快地获取数据,而无需重新计算或重新检索。这对于经常访问相同数据的应用程序来说尤其有用,因为它可以显著减少对慢速数据存储的依赖。

​ 总体而言,使用缓存是一种有效的性能优化手段,可以改善应用程序的响应速度、降低资源消耗,提升用户体验。

1.2 优势

  1. 提高访问速度:从缓存中获取数据比从原始数据源获取更快。
  2. 提高系统性能:通过将先前访问的数据存储在快速访问的内存中,减少了对慢速数据存储的依赖,从而加快数据访问速度
  3. 降低系统负载:减轻对数据库或其他资源的频繁访问压力,提高整体系统的稳定性和可伸缩性,减轻数据源压力
  4. 减少网络流量,尤其在分布式系统中,通过本地缓存避免了不必要的远程数据请求,降低了网络延迟

2、缓存使用场景

​ 使用缓存要注意合理设置缓存策略、处理缓存失效和更新,以确保缓存数据的一致性和可靠性。在具体应用中,选择合适的缓存方案需根据业务需求和系统特点来确定。

缓存的使用场景:

  1. 读密集型应用:对于读取操作频繁的应用,使用缓存可以显著提高性能,减少对数据存储的压力。
  2. 数据计算成本高昂:如果某些数据的计算成本很高,而且这些数据不经常变化,可以通过缓存计算结果,避免重复计算。
  3. 数据不经常变化:对于数据变化不频繁的场景,可以通过缓存来提高系统的响应速度。
  4. 数据库查询结果缓存: 缓存经常被用于存储数据库查询结果,特别是对于那些相对耗时的查询。通过缓存这些结果,可以显著降低对数据库的访问频率,提高系统的响应速度。
  5. Web页面和资源缓存: 在Web开发中,可以使用缓存存储静态页面、图片、CSS和JavaScript等资源,以减轻服务器和网络的负担,提高页面加载速度。
  6. 会话数据缓存: 缓存可以用于存储用户会话数据,以避免重复计算或查询。这对于需要维护用户状态的应用程序而言尤为有用。
  7. 接口响应缓存: 对于具有频繁且相对不变的接口响应,可以使用缓存存储已计算或获取的结果,以避免重复处理相同请求。
  8. 配置信息缓存: 缓存可以存储应用程序配置信息,以避免频繁读取配置文件或从数据库中检索配置数据。
  9. 全局数据共享: 在分布式系统中,缓存可以用于共享全局数据,以减少跨节点的通信和提高系统的可伸缩性。
  10. 热点数据缓存: 针对某些热点数据,即频繁访问的数据,使用缓存可以显著提高数据访问速度,减轻后端资源的压力。
  11. 频繁计算结果缓存: 对于计算成本较高的操作,将计算结果缓存以提高性能。

3、Java中的缓存实现方式

3.1 本地缓存

​ Java提供了一些基本的本地缓存实现,如HashMap、ConcurrentHashMap等。这些数据结构可以用于简单的内存缓存;但是这种方式只适用于轻量级的缓存需求,不具备持久性,应用重启后缓存数据会丢失。

​ ConcurrentHashMap是 Java 中的线程安全哈希表实现,相较于普通的 HashMap,ConcurrentHashMap提供了更好的并发性能,适用于多线程环境。然而,在一些特殊情况下可能需要额外的控制,例如在某些操作上保持原子性,可能需要使用更高级的原子操作或者结合其他的并发工具。

HashMap实现缓存代码演示:

package com.lmcode.hashmap;

import java.util.HashMap;
import java.util.Map;

public class SimpleLocalCache  {
    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();
    }
}
package com.lmcode.hashmap;

public class SimpleLocalCacheDemo1 {
    public static void main(String[] args) {
        SimpleLocalCache localCache = new SimpleLocalCache();
        // 添加数据到缓存
        localCache.put("key1", "value1");
        localCache.put("key2", "value2");
        // 从缓存中获取数据
        System.out.println("key1: " + localCache.get("key1"));
        System.out.println("key2: " + localCache.get("key2"));
        // 移除数据
        localCache.remove("key1");
        // 清空缓存
        localCache.clear();
        // 输出缓存大小
        System.out.println("Cache size: " + localCache.size());
    }
}

ConcurrentHashMap实现缓存代码演示:

package com.lmcode.hashmap;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentLocalCache {
    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();
    }
}
package com.lmcode.hashmap;

public class ConcurrentLocalCacheDemo1 {
    public static void main(String[] args) {
        ConcurrentLocalCache localCache = new ConcurrentLocalCache();
        localCache.put("key1", "value1");
        localCache.put("key2", "value2");
        System.out.println("key1: " + localCache.get("key1"));
        System.out.println("key2: " + localCache.get("key2"));
        System.out.println("Cache size: " + localCache.size());
    }
}

3.2 Java标准库的缓存支持

​ Java标准库通过java.util.concurrent包提供了一些用于构建缓存的工具类,其中的 ConcurrentMap 接口及其实现类是用于构建线程安全缓存的有力工具。ConcurrentMap提供了在多线程环境下进行安全并发操作的方法,确保对缓存的读写操作都是原子的。ConcurrentHashMap的设计使其适用于高并发的情况,通过使用分段锁可以实现更细粒度的并发控制,从而提高并发性能。

ConcurrentMap<String, Object> cache = new ConcurrentHashMap<>();

3.3 自定义缓存实现

​ 基于SoftReference或WeakReference的缓存实现提供了一种灵活的方式,使得在内存有限的情况下更加智能地管理对象。

​ SoftReference允许对象在内存不足时保持尽可能长的生命周期,适合于内存敏感的缓存场景,而WeakReference则更为弱化,对象在下一次垃圾回收时就可能被释放,这使得WeakReference适合于实现对缓存的短暂引用,只要没有强引用持有该对象,就会被回收。需要注意的是,使用WeakReference的缓存在一些情况下可能过于弱化,导致对象过早被回收。根据具体的使用场景,选择适合的引用类型是很重要的。

​ 这样的实现允许系统根据内存的实际需求动态管理缓存,避免内存溢出问题。选择合适的引用类型取决于具体的应用场景,是实现灵活、高效且可扩展的本地缓存的重要一环。

基于SoftReference的缓存代码演示:

package com.lmcode.basedOnReference;

import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class SoftReferenceCache<K, V> {
    private Map<K, SoftReference<V>> cache = new HashMap<>();

    /*
    * 将这个包装后的SoftReference对象和key放入cache中
    * 这样value的引用变成了SoftReference
    * 使得在内存不足时,垃圾回收系统可以回收 value 的内存。
    * */
    public void put(K key, V value) {
        //创建SoftReference对象,用于包装传入的value值。
        SoftReference<V> softReference = new SoftReference<>(value);
        cache.put(key, softReference);
        }

    /*
    * 如果 softReference不为null,则通过softReference.get()获取实际的值
    * 由于SoftReference可能已经被垃圾回收,因此 get()返回的值可能为 null。
    * 如果 softReference为null或者get()返回null,则表示缓存中没有与该键关联的值。
    * */
    public V get(K key) {
        //从cache中获取与key关联的SoftReference对象。
        SoftReference<V> softReference = cache.get(key);
        return (softReference != null) ? softReference.get() : null;
    }

    public void remove(K key) {
        cache.remove(key);
    }

    public void clear() {
        cache.clear();
    }

    public int size() {
        return cache.size();
    }
}

package com.lmcode.basedOnReference;

public class SoftReferenceCacheDemo1 {
    public static void main(String[] args) {
        SoftReferenceCache<String, String> softCache = new SoftReferenceCache<>();
        softCache.put("key1", "value1");
        System.out.println("key1: " + softCache.get("key1"));
        System.gc();// 触发垃圾回收
        System.out.println("key1: " + softCache.get("key1"));// 可能为null,因为对象在垃圾回收时被回收
    }
}

基于WeakReference的缓存代码演示:

package com.lmcode.basedOnReference;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

public class WeakReferenceCache<K, V> {
    private Map<K, WeakReference<V>> cache = new HashMap<>();

    /*
    * 将这个包装后的WeakReference对象和键key放入缓存cache中。
    * 这样,value的引用变成了WeakReference,使得在没有其他强引用持有对象时,
    * 垃圾回收系统可以回收 value 的内存。
    * */
    public void put(K key, V value) {
        // 创建WeakReference对象,用于包装传入的value值
        WeakReference<V> weakReference = new WeakReference<>(value);
        cache.put(key, weakReference);
    }

    /*
    * 如果weakReference不为null则通过weakReference.get()获取实际的值。
    * 因为WeakReference的引用弱化,如果没有其他强引用持有对象,get()返回的值可能为 null。
    * 如果weakReference为null或者get()返回null,则表示缓存中没有与该键关联的值。
    * */
    public V get(K key) {
        //从cache中获取与key关联的WeakReference对象。
        WeakReference<V> weakReference = cache.get(key);
        return (weakReference != null) ? weakReference.get() : null;
    }

    public void remove(K key) {
        cache.remove(key);
    }

    public void clear() {
        cache.clear();
    }

    public int size() {
        return cache.size();
    }
}

package com.lmcode.basedOnReference;

public class WeakReferenceCacheDemo1 {
    public static void main(String[] args) {
        WeakReferenceCache<String, String> weakCache = new WeakReferenceCache<>();
        weakCache.put("key1", "value1");
        System.out.println("key1: " + weakCache.get("key1"));
        System.gc();
        System.out.println("key1: " + weakCache.get("key1"));
    }
}

4、Java缓存框架

4.1 Redis

4.1.1 简介

​ Redis是一个开源的内存数据结构存储系统,作为数据库、缓存、消息代理等被广泛应用于软件项目中;它使用内存作为主要的数据存储介质,通过将数据存储在内存中,提供快速的读写操作。

​ 首先,Redis提供了多种数据结构,例如字符串、散列、列表、集合等;可以对这些数据类型运行原子操作,例如附加到字符串,增加哈希中的值,将元素推入列表,计算集合的交、并、差集,或获取排序集中排名最高的成员;可以用于解决各种实际问题,例如计数器、排行榜等。

​ 其次,Redis可以作为缓存层来提高系统的性能和响应速度。通过将经常访问的数据存储在内存中,可以大大减少对数据库的访问次数,从而提高系统的效率。

​ 最后,Redis还可以用作消息队列,用于实现异步通信和解耦系统组件。通过将消息存储在Redis中,可以实现不同组件之间的松耦合,提高系统的可扩展性和稳定性。

​ 与传统的磁盘存储系统相比,Redis具有更高的性能和低延迟;可以提供高性能、可扩展和可靠的数据存储和处理功能,所以redis在软件项目中的应用非常广泛。

4.1.2 Jedis简单使用
package com.lmcode.redis;

import redis.clients.jedis.Jedis;

public class demo1 {
    public static void main(String[] args) {
        // 连接到本地Redis服务器,默认端口为6379
        Jedis jedis = new Jedis("localhost", 6379);
        // 操作值
        jedis.set("key", "value");
        String value = jedis.get("key");
        System.out.println("key: " + value);
        // 操作列表
        jedis.lpush("list", "v1");
        jedis.lpush("list", "v2");
        jedis.lpush("list", "v3");
        System.out.println("list: " + jedis.lrange("list", 0, -1));
        // 关闭连接
        jedis.close();
    }
}
4.1.3 持久化方式

​ Redis支持两种主要的持久化方式,它们分别是RDB(Redis DataBase)快照和AOF(Append-Only File)日志文件。

​ RDB是Redis的默认持久化方式。它通过在指定的时间间隔内将内存中的数据集快照写入磁盘来实现持久化。这个过程是通过fork一个子进程,将数据集写入一个临时文件,然后用这个文件替换旧的快照文件。RDB持久化性能较好、只包含快照时刻的数据,文件较小;但是如果在快照之间发生故障,可能会有数据丢失。

​ AOF持久化是通过将每个写入命令追加到一个日志文件中来实现的。AOF文件是一个包含Redis服务器接收到的每个写入操作的日志文件,这些操作以Redis协议格式表示。AOF持久化可以有更少的数据丢失,而且AOF文件是以文本形式记录的,可读性更好,但是AOF文件通常比RDB文件更大,因为它包含了每个写入操作的详细信息,恢复时,需要逐行执行AOF文件中的命令,可能比加载一个RDB文件慢。

​ Redis有两种AOF持久化方式: always和everysec。always表示每个 Redis命令都会立即被写入AOF文件中,确保了最高级别的持久化,但会带来较高的性能开销。everysec 表示 Redis每秒钟将 AOF缓冲区中的命令写入 AOF文件中,这种方式在性能和持久化之间取得一个平衡。可以通过配置文件中的“appendfsync”选项来选择使用哪种AOF持久化方式。在默认情况下,Redis使用的是“everysec”方式。

4.1.4 异常情况

​ 通常 Redis 与关系型数据库搭配使用,在使用的过程中,可能会因使用不当而造成一些异常情况。下面以关系型数据库MySOL为例,列举了几个常见的 Redis 问题及解决方案。

1、缓存穿透

​ 假如某个key对应的数据在MySQL数据库中并不存在,则如果每次针对此key 的请求从 Redis 缓存获取不到,请求就会压到数据库 MySQL,从而可能压垮数据库。

​ 解决方案一:缓存空数据,查询返回的数据为空,仍然把这个空结果进行缓存“{key:1,value:null}”,简单,但是消耗内存,可能会发生不一致的问题。

​ 解决方案二:布隆过滤器,缓存预热时,预热布隆加载器,查询时先查询布隆过滤器,没有查到直接返回,布隆过滤器中查到的话再查Redis,Redis查不到查数据库;内存占用较少,没有多余key;但是实现复杂,存在误判

2、缓存击穿

​ 某个热点 key对应的数据存在,但在 Redis 中过期此时若有大量并发请求过来,则这些请求发现缓存过期后一般会从MySOL加载数据并回设到缓存,这时大并发的请求可能会瞬间把 MySOL压垮。

​ 解决方案一:互斥锁,强一致,性能差

​ 解决方案二:逻辑过期,高可用,性能优,不能保证数据绝对一致

// 获取锁
boolean lockAcquired = acquireLock(jedis);
if(lockAcquired){
    try{
        //执行需要加锁的操作

    }catch(InterruptedException e){
        e.printStackTrace();
    }finally{
        //释放锁
        releaseLock(jedis);
    }else{
        System.out.println("获取锁失败");
    }
    jedis.close();
}

3、缓存雪崩

​ 雪崩的场景就是 Redis 中较多的key在同一时间全部失效,这时大量请求会直接落到 MySQL上,造成数据库的压力过大,引发雪崩。

​ 解决方案:

  1. 给不同的Key的TTL添加随机值【大量的缓存key同时失效】
  2. 利用Redis集群提高服务的可用性【哨兵模式,集群模式】
  3. 给缓存业务添加降级限流策略【nginx或Spring cloud gatewat】【降级可做为系统的保底策略,适用于穿透、击穿、雪崩】
  4. 给业务添加多级缓存【Guava或Caffeine作为一级缓存】
4.1.5 总结

Redis是一种高性能的内存键值数据库,广泛应用于缓存、会话存储和实时分析等场景。其特点包括快速的读写操作、支持丰富的数据结构、持久化支持(RDB和AOF)、分布式支持,以及强大的原子性操作。

4.2 Ehcache

4.2.1 简介

​ Ehcache是一款开源的Java分布式缓存框架,适用于单机和分布式环境,广泛应用于Java应用程序中。它的设计旨在提升应用性能并降低数据库负载。通过在内存中存储常用数据,Ehcache实现了快速检索,加速应用程序的响应时间。该框架支持多种缓存策略,如LRU(最近最少使用)、FIFO(先进先出)等,使开发者能够根据应用需求选择适合的缓存管理方式。

​ 使用Ehcache,开发者可以轻松地将缓存集成到现有的应用中,提高数据访问效率,并有效减轻后端数据库的压力。除此之外,Ehcache提供了灵活的配置选项,允许开发者根据具体场景进行调整和优化,使其适应不同的应用需求。总体而言,Ehcache作为一款功能丰富的Java缓存框架,为开发者提供了强大而灵活的工具,以提升应用性能和用户体验。

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>
4.2.2 基本使用

首先需要定义一个 CacheManager,然后通过它创建或获取缓存,最后在缓存中存储和检索数据

package com.lmcode.ehcache;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;

public class EhcacheExample {
    public static void main(String[] args) {
        // 用 CacheManagerBuilder创建一个缓存管理器
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);

        // 定义缓存配置
        Cache<String, String> myCache = cacheManager.createCache("myCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class,
                                ResourcePoolsBuilder.heap(10)) // 不再直接设置堆内存大小
                        .build());

        myCache.put("key1", "value1");
        String value = myCache.get("key1");
        System.out.println("key1: " + value);
        // 关闭缓存管理器
        cacheManager.close();
    }
}
4.2.3 缓存配置
Java配置

使用 Ehcache 时,通常需要定义一个类来存储缓存的配置信息,这样可以更灵活地配置和管理缓存。

创建一个名为 MyCacheConfig的类,用于存储 Ehcache 缓存的配置信息。

package com.lmcode.ehcache;

import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;


public class MyCacheConfig {
    private String cacheName;
    private Class<?> keyType;
    private Class<?> valueType;
    private int heapSize;

    // 构造函数,用于初始化缓存配置信息
    public MyCacheConfig(String cacheName, Class<?> keyType, Class<?> valueType, int heapSize) {
        this.cacheName = cacheName;
        this.keyType = keyType;
        this.valueType = valueType;
        this.heapSize = heapSize;
    }
    // 获取缓存名称
    public String getCacheName() {
        return cacheName;
    }
    // 获取键的类型
    public Class<?> getKeyType() {
        return keyType;
    }
    // 获取值的类型
    public Class<?> getValueType() {
        return valueType;
    }
    // 获取堆内存大小
    public int getHeapSize() {
        return heapSize;
    }
    // 创建并返回 Ehcache 缓存的配置构建器
    public <K, V> CacheConfigurationBuilder<K, V> getCacheConfigurationBuilder() {
        return CacheConfigurationBuilder.newCacheConfigurationBuilder(
                (Class<K>) getKeyType(), (Class<V>) getValueType(),
                ResourcePoolsBuilder.heap(getHeapSize()));
    }
}
package com.lmcode.ehcache;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheManagerBuilder;

public class EhcacheWithPOJO {
    public static void main(String[] args) {
        // 创建缓存管理器
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
        // 创建缓存配置的 POJO 实例
        MyCacheConfig cacheConfig = new MyCacheConfig("myCache", Long.class, String.class, 10);
        // 使用 POJO 配置创建缓存
        Cache<Object, Object> myCache = cacheManager.createCache(
                cacheConfig.getCacheName(),
                cacheConfig.getCacheConfigurationBuilder().build());

        myCache.put(1L, "value1");
        String value = (String) myCache.get(1L);
        System.out.println("1L:" + value);
        cacheManager.close();
    }
}
XML配置
<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>
URL configFileUrl = EhcacheWithXML.class.getClassLoader().getResource("com/lmcode/ehcache.xml");
if (configFileUrl == null) {
    throw new IllegalStateException("找不到文件");
}
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .with(CacheManagerBuilder.persistence("ehcache.xml"))
    .build(true);
4.2.4 其他Ehcache配置选项
磁盘持久化

如果有太多值无法存储到缓存中,我们可以将其中一些值存储在硬盘上。

package com.lmcode.ehcache;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;

import java.io.File;
import java.util.concurrent.TimeUnit;

public class PersistentCacheExample {

    public static void main(String[] args) {
        // 指定持久化存储路径
        String storagePath = "/path/to/persistent/cache";

        // 创建缓存管理器
        /* 配置持久化存储,指定磁盘路径 */
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                .with(CacheManagerBuilder.persistence(new File(storagePath)))
                .build(true);

        // 配置缓存
        CacheConfiguration<Integer, String> cacheConfiguration = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(Integer.class, String.class,
                        ResourcePoolsBuilder.newResourcePoolsBuilder()
                                .heap(10, EntryUnit.ENTRIES) // 设置堆内存大小
                                .disk(10, MemoryUnit.MB, true) // 启用磁盘存储,设置磁盘大小
                )
                .withExpiry(Expirations.timeToLiveExpiration(Duration.of(30, TimeUnit.SECONDS))) // 设置缓存过期时间
                .build();


        Cache<Integer, String> cache = cacheManager.createCache("persistent-cache", cacheConfiguration);
        cache.put(1, "Value1");
        cache.put(2, "Value2");
        String value1 = cache.get(1);
        String value2 = cache.get(2);
        System.out.println("1: " + value1);
        System.out.println("2: " + value2);
        cacheManager.close();
    }
}
数据过期

如果缓存大量诗句,可以设置缓存数据保存一段时间然后过期,避免大量的内存使用

配置在缓存中,所有数据只存活 60 秒,过期将从内存中删除。

CacheConfiguration<Integer, Integer> cacheConfiguration 
  = CacheConfigurationBuilder
    .newCacheConfigurationBuilder(Integer.class, Integer.class, 
      ResourcePoolsBuilder.heap(100)) 
    .withExpiry(Expirations.timeToLiveExpiration(Duration.of(60, 
      TimeUnit.SECONDS))).build();
4.2.5 总结

在 Java 应用程序中使用简单的 Ehcache 缓存。

即使是简单配置的缓存也可以节省很多不必要的操作。此外,还可以通过 POJO 和 XML 配置缓存,并且 Ehcache 具有一些不错的功能,例如持久性和数据过期。

4.3 Caffeine

4.3.1 简介

​ Caffeine是一个用于java的高性能缓存库,可以提供高性能、灵活且易于使用的本地缓存;具有强大的功能,支持定时失效、大小限制等特性。

​ Caffeine采用了许多性能优化策略,例如精细的数据结构和高效的内存管理,以确保在高负载情况下也能够提供快速的缓存访问;Caffeine还提供了丰富的配置选项,可以根据应用程序的需求进行调整。例如缓存的最大大小,过期时间,刷新时间等;Caffeine还支持同步和异步两种加载数据的方式。通过同步加载可以在主线程中阻塞加载数据;而异步加载则允许在数据加载时不阻塞主线程,提高系统的并发性;Caffeine支持多种过期策略来管理缓存中的数据,例如基于时间或基于访问频率;Caffeine还可以注册监听器,以便在缓存发生变化时得到通知。包括数据被移除、加载新数据等情况,这为应用程序提供了更细粒度的控制和监视。

<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
   <version>3.0.2</version>
</dependency>
4.3.2 三种缓存填充策略
手动填充
package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

/*手动填充*/
public class CaffeineExample1 {
    public static void main(String[] args) {
        // 创建一个 Caffeine 缓存
        Cache<String, String> cache = Caffeine.newBuilder().maximumSize(100).build();
        // 向缓存中添加数据
        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");

        // 从缓存中获取数据
        String value1 = cache.getIfPresent("key1");
        System.out.println("key1:"+value1);

        // 如果缓存中不存在该键,则提供一个默认值
        String value4 = cache.get("key4", k -> "value4_default");
        System.out.println("key4:" + value4);

        // 移除缓存中的数据
        cache.invalidate("key2");

        // 打印缓存中的所有键值对
        cache.asMap().forEach((key, value) -> System.out.println(key + ": " + value));
    }
}
同步加载

缓存中没有 key1 对应的值,此时触发同步加载;如果缓存中存在键为 "key1" 的值,那么直接返回该值;

如果缓存中不存在 "key1" 对应的值,就执行传入的 Lambda 表达式,返回 "Value1_loaded"。

package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

/*同步加载*/
public class CaffeineExample2 {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder().build();
        String value1 = cache.get("key1", key -> {
            // 在缓存中缺失时,同步加载数据的逻辑
            return "Value1_loaded";
        });
        System.out.println("key1:" + value1);
    }
}
异步加载
package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.CompletableFuture;

/*异步加载*/
public class CaffeineExample3 {
    public static void main(String[] args) {
        AsyncLoadingCache<Object, Object> asyncCache = Caffeine.newBuilder()
                .buildAsync(key -> {
                    // 在缓存中缺失时,异步加载数据的逻辑
                    // 异步加载的结果
                    return CompletableFuture.completedFuture("Value_loaded");
                });

        // 缓存中没有 key1 对应的值,此时触发异步加载
        CompletableFuture<Object> futureValue2 = asyncCache.get("key1");
        futureValue2.thenAccept(value -> System.out.println("key1: " + value));

        // 如果缓存中不存在该键,则提供一个默认值
        CompletableFuture<Object> futureValue4 = asyncCache.get("key2", k -> CompletableFuture.completedFuture("default"));
        System.out.println("key2: " + futureValue4.join());// 使用 join() 获取结果
    }
}
4.3.3 三种删除策略
基于大小的删除策略

这种策略是基于缓存的大小来进行驱逐的。通过设置 maximumSize 参数来限制缓存的最大条目数量,这个数量是近似的,因为 Caffeine 缓存是基于权重进行管理的,而不是基于实际条目的数量。默认情况下,每个条目的权重是1。当缓存达到这个限制时,Caffeine 将根据一定的策略从缓存中移除一些条目,以腾出空间。

获取缓存大小之前调用cleanUp方法。这是因为缓存逐出是异步执行的,此方法有助于等待逐出完成。

package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Eviction1 {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1)  // 设置最大缓存条目数量
                .build();
        cache.put("key1", "value1");
        String value1 = cache.getIfPresent("key1");
        cache.cleanUp();

        System.out.println("key1:"+value1);
        System.out.println("加入key1后的估计大小: " + cache.estimatedSize());

        cache.put("key2", "value2");  // 这里会触发缓存驱逐,因为超过了最大条目数量
        String value2 = cache.getIfPresent("key2");
        cache.cleanUp();

        System.out.println("key2:"+value2);
        System.out.println("加入key2后的估计大小: " + cache.estimatedSize());

        // 打印缓存中的所有键值对
        System.out.println("--------");
        cache.asMap().forEach((key, value) -> System.out.println(key + ": " + value));
    }
}
基于时间的删除策略

缓存条目在最后一次访问之后经过1分钟后过期:

package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class Eviction2Test {
    public static void main(String[] args) throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.MINUTES)
                .build();
        cache.put("key1", "value1");
        String value1 = cache.getIfPresent("key1");

        System.out.println("估计的缓存对象数: " + cache.estimatedSize());
        // 等待1分钟,以验证缓存的过期时间
        Thread.sleep(TimeUnit.MINUTES.toMillis(1) + 1000);
        String value1AfterExpiry = cache.getIfPresent("key1");
        System.out.println("key1 after expiry: " + value1AfterExpiry);
    }
}

写入后一分钟过期:

Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build();

还可以设置缓存键和值使用弱引用,即如果没有其他强引用指向缓存的键或值,它们将会被垃圾回收。

这样的设置适用于某些场景,例如对于缓存中的对象,如果在外部没有其他强引用时,希望缓存中的对象能够被垃圾回收。这样,当缓存中的对象变得不再可访问时,它们会被及时地释放。

cache = Caffeine.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .weakKeys()
    .weakValues()
    .build();

自定义过期策略,需要实现Expiry接口的方法来自定义缓存条目的过期时间。

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
    @Override
    public long expireAfterCreate(
      String key, DataObject value, long currentTime) {
        return value.getData().length() * 1000;
    }
    @Override
    public long expireAfterUpdate(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build();
  • expireAfterCreate: 当缓存中创建一个新的条目时,这个方法被调用,返回的时间值表示这个条目的过期时间。在这个例子中,过期时间是根据 DataObject 对象中的数据长度来计算的,单位是毫秒。

  • expireAfterUpdate: 当缓存中的条目被更新时,这个方法被调用。在这个例子中,它返回当前持续时间,表示条目的过期时间不受更新的影响。

  • expireAfterRead: 当缓存中的条目被访问时,这个方法被调用。在这个例子中,它返回当前持续时间,表示条目的过期时间不受读取的影响。

基于参考的删除策略

配置缓存以允许对缓存键或值进行垃圾收集,可以为键和值配置WeakReference的使用,并且可以将SoftReference配置为仅用于值的垃圾收集。

当没有任何对对象的强引用时,WeakRefence 的使用允许对对象进行垃圾收集。

SoftReference 允许根据 JVM 的全局最近最少使用策略对对象进行垃圾收集。


使用了弱引用键和弱引用值。这意味着如果没有其他强引用指向缓存的键或值,它们可能会被垃圾回收。

Cache<Object, Object> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.SECONDS)
    .weakKeys()
    .weakValues()
    .build();

使用了软引用值。软引用值表示在系统内存不足时,这些值可能被垃圾回收。

Cache<Object, Object> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.SECONDS)
    .softValues()
    .build();
4.3.4 刷新

在定义的时间段后自动刷新条目,即使数据未过期,也会触发异步加载数据的刷新操作;如果该条目符合刷新条件,则缓存将返回旧值并异步重新加载该值。

适用于在缓存数据过期后,当有请求访问数据时,能够异步地重新加载最新数据的场景。这样可以保持缓存中的数据相对新鲜。

Caffeine.newBuilder()
    //在写入条目后的1分钟内,如果有请求访问某个键,将会触发异步加载数据的操作。
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build());
4.3.5 统计

记录有关缓存使用情况的统计信息:

package com.lmcode.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Statistics {
public static void main(String[] args) {
    Cache<Object, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .recordStats()
            .build();

    cache.put("key1", "value1");
    cache.put("key2", "value1");
    String value1 = (String) cache.getIfPresent("key1");
    String value2 = (String) cache.getIfPresent("key2");

    // 输出统计信息
    System.out.println("Hit count: " + cache.stats().hitCount());  //命中数
    System.out.println("Miss count: " + cache.stats().missCount());  //未命中数
    }
}
4.3.6 总结

使用Caffeine主要是了解如何配置和填充缓存,根据需要选择适当的过期或刷新策略

总结

​ Java中的缓存是提高系统性能的重要工具。开发者可以根据具体场景选择合适的缓存实现方式和框架。通过合理使用缓存,可以在保证系统性能的同时降低对数据源的访问压力,从而提升用户体验。在选择缓存框架时,需要考虑框架的功能特性、性能、易用性等因素,以更好地满足项目需求。

​ 缓存是一种广泛应用于软件开发中的技术,通过存储先前访问的数据副本来提高系统性能。它有效降低了对慢速数据存储的访问频率,加速数据检索,提高应用程序的响应速度。常见的缓存应用场景包括数据库查询结果、Web页面和资源、会话数据、接口响应以及全局数据共享。合理使用缓存可以减轻服务器负载,提高系统的可伸缩性,并改善用户体验。

​ 然而,缓存的设计和管理需要考虑缓存策略、数据一致性、缓存失效处理等方面,以确保缓存的有效性和可靠性。在不同应用场景下选择适当的缓存方案是关键,能够为应用程序提供更高效的数据访问和处理能力。