MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于Guava Cache的本地缓存实现技巧

2021-06-036.2k 阅读

1. Guava Cache简介

Guava Cache是Google Guava库中提供的一个本地缓存实现。它提供了一种简单而强大的方式来管理本地缓存,适用于许多后端开发场景。与分布式缓存(如Redis)不同,Guava Cache是基于应用程序本地内存的缓存,这意味着它的性能非常高,因为不需要通过网络来访问缓存数据。然而,它的局限性在于缓存数据只存在于单个应用实例中,如果应用是分布式部署的,每个实例都有自己独立的Guava Cache,这在某些情况下可能需要额外的处理来保持缓存一致性。

Guava Cache提供了很多特性,包括自动加载缓存数据、缓存过期策略、缓存淘汰策略等。这些特性使得开发人员可以根据具体的业务需求灵活配置缓存行为。

2. 引入Guava依赖

要使用Guava Cache,首先需要在项目中引入Guava库的依赖。如果使用Maven来管理项目依赖,可以在pom.xml文件中添加如下依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

上述依赖中,groupIdcom.google.guavaartifactIdguava,版本号为31.1-jre,你可以根据实际情况选择合适的版本。如果使用Gradle来管理依赖,在build.gradle文件中添加如下内容:

implementation 'com.google.guava:guava:31.1-jre'

这样就完成了Guava库的引入,项目中就可以使用Guava Cache相关的功能了。

3. 创建简单的Guava Cache

创建一个基本的Guava Cache非常简单。以下是一个示例代码,展示了如何创建一个简单的缓存并向其中放入和获取数据:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class SimpleGuavaCacheExample {
    public static void main(String[] args) {
        // 创建一个缓存构建器
        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
        // 使用构建器创建缓存
        Cache<Integer, String> cache = cacheBuilder.build();

        // 向缓存中放入数据
        cache.put(1, "value1");

        // 从缓存中获取数据
        String value = cache.getIfPresent(1);
        System.out.println("从缓存中获取到的值: " + value);
    }
}

在上述代码中,首先通过CacheBuilder.newBuilder()创建了一个CacheBuilder对象。CacheBuilder提供了一系列方法来配置缓存的特性,这里我们没有对其进行额外配置,直接使用build()方法构建出了一个Cache对象。然后,通过put方法向缓存中放入一个键值对,键为1,值为"value1"。最后,使用getIfPresent方法从缓存中获取对应键的值,并打印出来。

4. 缓存的自动加载

Guava Cache支持自动加载缓存数据。这意味着当从缓存中获取一个不存在的键时,Guava Cache可以自动调用一个加载方法来获取数据,并将其放入缓存中。这种机制非常方便,特别是在缓存数据的获取相对复杂的情况下。

下面是一个示例,展示了如何实现缓存的自动加载:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ExecutionException;

public class LoadingCacheExample {
    public static void main(String[] args) {
        // 创建一个带有自动加载功能的缓存
        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
               .build(new CacheLoader<Integer, String>() {
                    @Override
                    public String load(Integer key) throws Exception {
                        // 模拟从数据库或其他数据源加载数据
                        return "data for key " + key;
                    }
                });

        try {
            // 获取缓存数据,如果不存在则自动加载
            String value = cache.get(1);
            System.out.println("获取到的值: " + value);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过CacheBuilder.build(CacheLoader)方法创建了一个LoadingCache对象。CacheLoader是一个抽象类,需要实现其中的load方法,该方法负责从数据源加载数据。在main方法中,通过cache.get(1)获取缓存数据,如果键1对应的缓存数据不存在,就会调用CacheLoaderload方法加载数据,并将其放入缓存中。

5. 缓存过期策略

Guava Cache支持多种缓存过期策略,主要包括基于时间的过期和基于访问的过期。

5.1 基于时间的过期

基于时间的过期又分为两种:写入后过期(expireAfterWrite)和访问后过期(expireAfterAccess)。

写入后过期:表示从缓存数据被写入缓存开始,经过指定的时间后,该缓存数据就会过期。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class ExpireAfterWriteExample {
    public static void main(String[] args) {
        // 创建一个缓存,设置写入后10秒过期
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .expireAfterWrite(10, TimeUnit.SECONDS)
               .build();

        cache.put(1, "value1");

        try {
            // 等待11秒
            TimeUnit.SECONDS.sleep(11);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String value = cache.getIfPresent(1);
        System.out.println("11秒后获取到的值: " + value);
    }
}

在上述代码中,通过expireAfterWrite(10, TimeUnit.SECONDS)设置了缓存数据在写入后10秒过期。程序先向缓存中放入数据,然后等待11秒,再获取缓存数据,此时应该获取不到,打印出的结果为null

访问后过期:表示从缓存数据最后一次被访问开始,经过指定的时间后,该缓存数据就会过期。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class ExpireAfterAccessExample {
    public static void main(String[] args) {
        // 创建一个缓存,设置访问后10秒过期
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .expireAfterAccess(10, TimeUnit.SECONDS)
               .build();

        cache.put(1, "value1");

        try {
            // 等待5秒后访问一次缓存
            TimeUnit.SECONDS.sleep(5);
            cache.getIfPresent(1);

            // 再等待6秒
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String value = cache.getIfPresent(1);
        System.out.println("11秒后获取到的值: " + value);
    }
}

在这个示例中,通过expireAfterAccess(10, TimeUnit.SECONDS)设置了缓存数据在访问后10秒过期。程序先向缓存中放入数据,等待5秒后访问一次缓存,然后再等待6秒,此时距离最后一次访问已经超过10秒,再次获取缓存数据时应该获取不到,打印结果为null

5.2 基于引用的过期

Guava Cache还支持基于引用的过期策略,包括软引用(soft references)和弱引用(weak references)。

软引用:当系统内存不足时,软引用关联的对象可能会被垃圾回收器回收。使用软引用可以有效地防止内存溢出,因为在内存紧张时,缓存数据会被自动清理。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class SoftReferenceCacheExample {
    public static void main(String[] args) {
        // 创建一个使用软引用的缓存
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .softValues()
               .build();

        cache.put(1, "value1");
        // 获取缓存数据
        String value = cache.getIfPresent(1);
        System.out.println("获取到的值: " + value);
    }
}

在上述代码中,通过softValues()方法设置缓存的值使用软引用。这样当系统内存不足时,缓存中的值可能会被回收。

弱引用:只要垃圾回收器发现弱引用关联的对象没有其他强引用指向它,就会立即回收该对象。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class WeakReferenceCacheExample {
    public static void main(String[] args) {
        // 创建一个使用弱引用的缓存
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .weakValues()
               .build();

        cache.put(1, "value1");
        // 获取缓存数据
        String value = cache.getIfPresent(1);
        System.out.println("获取到的值: " + value);
    }
}

这里通过weakValues()方法设置缓存的值使用弱引用。与软引用不同,弱引用对象的回收不依赖于系统内存是否紧张,只要没有强引用指向它,就会被回收。

6. 缓存淘汰策略

除了过期策略外,Guava Cache还提供了缓存淘汰策略,用于在缓存容量达到上限时决定淘汰哪些缓存数据。

6.1 LRU(最近最少使用)策略

LRU策略是一种常见的缓存淘汰策略,它会淘汰最近最少使用的缓存数据。在Guava Cache中,使用CacheBuildermaximumSize方法设置缓存的最大容量,并默认采用LRU策略。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class LRUCacheExample {
    public static void main(String[] args) {
        // 创建一个最大容量为3的LRU缓存
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .maximumSize(3)
               .build();

        cache.put(1, "value1");
        cache.put(2, "value2");
        cache.put(3, "value3");

        // 访问键2
        cache.getIfPresent(2);

        cache.put(4, "value4");

        String value1 = cache.getIfPresent(1);
        String value2 = cache.getIfPresent(2);
        String value3 = cache.getIfPresent(3);
        String value4 = cache.getIfPresent(4);

        System.out.println("键1的值: " + value1);
        System.out.println("键2的值: " + value2);
        System.out.println("键3的值: " + value3);
        System.out.println("键4的值: " + value4);
    }
}

在上述代码中,通过maximumSize(3)设置缓存的最大容量为3。先向缓存中放入3个键值对,然后访问键2,使其成为最近使用的。接着再放入键4,此时缓存容量达到上限,根据LRU策略,最近最少使用的键1对应的缓存数据会被淘汰。最后打印各个键对应的值,可以看到键1的值为null,其他键的值可以正常获取。

6.2 LFU(最不经常使用)策略

LFU策略会淘汰最不经常使用的缓存数据。在Guava Cache中,可以通过CacheBuilderbuild方法传入一个CacheLoader,并结合removalListener来实现类似LFU的效果。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;

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

public class LFUCacheExample {
    private static Map<Integer, Integer> usageCountMap = new HashMap<>();

    public static void main(String[] args) {
        RemovalListener<Integer, String> removalListener = new RemovalListener<Integer, String>() {
            @Override
            public void onRemoval(RemovalNotification<Integer, String> notification) {
                if (notification.wasEvicted()) {
                    usageCountMap.remove(notification.getKey());
                }
            }
        };

        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
               .maximumSize(3)
               .removalListener(removalListener)
               .build(new CacheLoader<Integer, String>() {
                    @Override
                    public String load(Integer key) throws Exception {
                        return "data for key " + key;
                    }
                });

        try {
            cache.get(1);
            cache.get(2);
            cache.get(1);
            cache.get(3);
            cache.get(2);
            cache.get(4);

            String value1 = cache.getIfPresent(1);
            String value2 = cache.getIfPresent(2);
            String value3 = cache.getIfPresent(3);
            String value4 = cache.getIfPresent(4);

            System.out.println("键1的值: " + value1);
            System.out.println("键2的值: " + value2);
            System.out.println("键3的值: " + value3);
            System.out.println("键4的值: " + value4);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,定义了一个usageCountMap来记录每个键的使用次数。通过removalListener在缓存数据被淘汰时从usageCountMap中移除对应的记录。每次获取缓存数据时,增加对应键的使用次数。当缓存容量达到上限时,淘汰使用次数最少的键对应的缓存数据。

7. 缓存统计信息

Guava Cache提供了获取缓存统计信息的功能,这些统计信息可以帮助开发人员了解缓存的使用情况,以便进行性能优化。可以通过Cache.stats()方法获取缓存的统计信息。示例代码如下:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheStats;

public class CacheStatsExample {
    public static void main(String[] args) {
        Cache<Integer, String> cache = CacheBuilder.newBuilder()
               .build();

        cache.put(1, "value1");
        cache.getIfPresent(1);

        CacheStats stats = cache.stats();
        System.out.println("命中次数: " + stats.hitCount());
        System.out.println("未命中次数: " + stats.missCount());
        System.out.println("命中率: " + stats.hitRate());
    }
}

在上述代码中,先向缓存中放入数据并获取一次。然后通过cache.stats()获取缓存的统计信息,CacheStats对象提供了诸如hitCount()(命中次数)、missCount()(未命中次数)、hitRate()(命中率)等方法,通过这些方法可以打印出缓存的相关统计信息。

8. 与其他缓存技术的结合使用

在实际开发中,有时可能需要将Guava Cache与其他缓存技术(如Redis)结合使用,以发挥各自的优势。例如,可以将经常访问且数据相对稳定的热点数据放在Guava Cache中,利用其高性能的本地缓存特性;而对于需要在多个应用实例间共享的缓存数据,或者数据量较大不适合全部放在本地内存的情况,可以使用Redis。

以下是一个简单的示例,展示了如何在获取数据时先尝试从Guava Cache中获取,如果未命中则从Redis中获取,并将获取到的数据放入Guava Cache:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import redis.clients.jedis.Jedis;

public class CompositeCacheExample {
    private static Cache<Integer, String> guavaCache = CacheBuilder.newBuilder()
           .build();

    public static String getData(int key) {
        String value = guavaCache.getIfPresent(key);
        if (value != null) {
            return value;
        }

        try (Jedis jedis = new Jedis("localhost")) {
            value = jedis.get(String.valueOf(key));
            if (value != null) {
                guavaCache.put(key, value);
            }
        }

        return value;
    }

    public static void main(String[] args) {
        String data = getData(1);
        System.out.println("获取到的数据: " + data);
    }
}

在上述代码中,定义了一个guavaCachegetData方法先从guavaCache中尝试获取数据,如果未命中,则连接到本地的Redis实例获取数据,并将从Redis获取到的数据放入guavaCache中。这样可以在一定程度上提高数据获取的性能,同时兼顾数据的共享性。

9. 注意事项

在使用Guava Cache时,有一些注意事项需要关注。

首先,由于Guava Cache是基于本地内存的缓存,缓存数据量不宜过大,否则可能会导致应用程序内存溢出。需要根据实际应用场景和服务器内存情况合理设置缓存的最大容量。

其次,在分布式应用中使用Guava Cache时,每个应用实例都有自己独立的缓存,这可能会导致缓存不一致的问题。如果需要在多个实例间保持缓存一致性,可能需要结合分布式缓存(如Redis)或者采用其他机制(如发布 - 订阅模式)来通知各个实例更新缓存。

另外,虽然Guava Cache提供了丰富的过期和淘汰策略,但在设置这些策略时需要谨慎考虑业务需求。例如,过期时间设置过短可能导致缓存命中率降低,增加后端数据源的负载;而过期时间设置过长可能导致缓存数据长时间不更新,出现数据不一致的情况。

在缓存数据的类型选择上,要确保缓存的数据类型是可序列化的(如果需要使用缓存的持久化等功能),并且在缓存数据的生命周期内,数据的结构和内容不会发生意外变化,以免导致缓存数据的错误使用。

通过合理运用Guava Cache的各种特性,并注意上述事项,可以在后端开发中有效地利用本地缓存提高应用程序的性能和响应速度。