1、背景

【问】:项目中哪些地方会用到缓存?为什么要引入缓存?如何使用缓存?引入缓存后会带来哪些问题?

还得结合项目中的业务来回答。引入缓存,其实主要有两个用途:高性能、高并发

假设:某个操作非常频繁。比如:网站的商城首页 —— 需要频繁的从数据库里面获取商品数据,可能从数据库一顿各种操作下来,平均耗时 500 ms,随着请求频次越高,用户等待数据的返回结果时间越来越长,体验越来越差

如果此时,引入缓存,将数据库里面查询出来的商品数据信息,放入缓存服务里面,当用户再此发起查询操作的时候,直接从缓存服务里面获取,速度从耗时 500 ms,可能直接优化成 5 ms,体验上瞬间会上升好几个层次! —— 这就是引入缓存带来的高性能体验结果

引入缓存之前,以 mysql 数据库为例,单台机器一秒内的请求次数到达 2000 之后就会开始报警;引入缓存之后,以 redis 缓存服务器为例,单台机器一秒内的请求次数支持 110000 次,两者支持的并发量完全不是一个数量级的 —— 这就是引入缓存带来的高并发体验结果

尤其是对于流量很大的业务,引入缓存,给系统带来的提升是十分显著的

【问】:缓存和数据库为什么差距这么大?有什么区别?

在计算机中,数据的存储主要有两处:内存;磁盘

  • 内存:内存的数据读写性能 远超磁盘的读写性能;虽然读写性能非常高,但是当电脑重启之后,数据会全部清除
  • 磁盘:存入磁盘的数据,虽然读写性能很差,但是电脑重启之后数据不会丢失

因为两者的数据存储方案不同,造就了不同的实践用途

在项目中如何引入缓存呢?通常的做法如下:

  1. 当用户发起访问某数据的操作时,检查缓存服务里面是否存在,如果存在,直接返回;如果不存在,走数据库的查询服务
  2. 从数据库里面获取到有效数据之后,存入缓存服务,并返回给用户
  3. 当被访问的数据发生更新的时候,需要同时删除缓存服务,以便用户再次查询的时候,能获取到最新的数据

这对于简单的需要缓存的业务场景,能轻松应对。

但是面对复杂的业务场景和服务架构,尤其是对缓存要求比较高的业务,引入缓存的方式,也会跟着一起变化。

根据缓存面向的对象不同,缓存分为:本地缓存、分布式缓存和多级缓存

  • 本地缓存:在单个计算机服务实例中,直接把数据缓存到内存中进行使用

本地缓存是直接从本地内存中读取,没有网络开销。例如:秒杀系统或者数据量小的缓存等,比远程缓存更合适

  • 分布式缓存:现在的服务,大多都是以集群的方式来部署。即:同一个网站服务,同时在两台计算机里面部署,如用到的 session 会话,就无法同时共享,因此需要引入一个独立的缓存服务来连接两台服务器,这个独立部署的缓存服务就是分布式缓存
  • 多级缓存:在实际的业务中,本地缓存和分布式缓存会同时结合进行使用,当收到访问某个数据的操作时,会优先从本地缓存服务(也叫一级缓存)查询,如果没有,再从分布式缓存服务(也叫二级缓存)里面获取,如果也没有,最后再从数据库里面获取;从数据库查询完成之后,在依次更新分布式缓存服务、本地缓存服务

2、手写一个简单的本地缓存

使用缓存的时候,比较关注两个地方:

  1. 内存持久化
  2. 支持缓存的数据自动过期清除

对于简单的数据缓存,我们完全可以自行编写一套缓存服务,实现过程如下:

①:创建一个缓存实体类

@Data
public class CacheEntity {
    // 缓存键
    private String key;
    // 缓存键
    private Object value;
    // 过期时间
    private Long expireTime; 
}

②:缓存工具类

public class CacheUtil {

    // 缓存数据
    private final static Map<String, CacheEntity> CACHE_MAP = new ConcurrentHashMap<>();

    // 定时器线程池,用于清理过期缓存
    private static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    static {
        // 注册一个定时任务,服务启动 1000 毫秒后,每隔 500 毫秒执行一次
        Runnable task = CacheUtil::clear;
        executorService.scheduleAtFixedRate(task, 1000L, 500L, TimeUnit.MILLISECONDS);
    }
	
	// 添加缓存
    public static void put(String key, Object value) {
        put(key, value, 0L);
    }
	
	// 添加缓存
    public static void put(String key, Object value, Long expire) {
        CacheEntity cacheEntity = new CacheEntity();
        cacheEntity.setKey(key);
        cacheEntity.setValue(value);
        if (expire > 0) {
        	// 计算过期时间
            Long expireTime = System.currentTimeMillis() + Duration.ofSeconds(expire).toMillis();
            cacheEntity.setExpireTime(expireTime);
        }
        CACHE_MAP.put(key, cacheEntity);
    }
	
	// 获取
    public static Object get(String key) {
        if (CACHE_MAP.containsKey(key)) {
            return CACHE_MAP.get(key);
        }
        return null;
    }
	
	// 删除
    public static void remove(String key) {
        CACHE_MAP.remove(key);
    }
	
	// 清除过期缓存
    public static void clear() {
        if (CACHE_MAP.isEmpty()) {
            return;
        }
        CACHE_MAP.entrySet().removeIf(entityEntry -> entityEntry.getValue().getExpireTime() != null && entityEntry.getValue().getExpireTime() > System.currentTimeMillis());
    }

}

实现思路:采用 ConcurrentHashMap 作为缓存数据存储服务,然后开启一个定时调度,每隔 500 毫秒检查一下过期的缓存数据,然后清除掉

③:测试

public class Test {

    public static void main(String[] args) throws InterruptedException {
        CacheUtil.put("name", "zzc", 10L);
        System.out.println("第一次查询结果:" + CacheUtil.get("name"));
        Thread.sleep(2000L);
        System.out.println("第二次查询结果:" + CacheUtil.get("name"));
    }
}

3、Guava Cache 简介

Guava github
美团技术团队-缓存那些事

Guava cache 是 Google 开发的 Guava 工具包中一套完善的 JVM 本地缓存框架,底层实现的数据结构类似于 ConcurrentHashMap,但是进行了更多的能力拓展,包括缓存过期时间设置、缓存容量设置、多种淘汰策略、缓存监控等。

Guava cache 是一个支持高并发的线程安全的本地缓存。多线程情况下也可以安全的访问或者更新Cache。这些都是借鉴了 ConcurrentHashMap 的结果,不过,Guava cache 又有自己的特性 :

  1. 支持最大容量限制
  2. 支持两种过期删除策略(插入时间和读取时间)
  3. 支持简单的统计功能
  4. 基于 LRU 算法实现

4、Guava Cache 使用

缓存分享(1)——Guava Cache原理及最佳实践

使用 —— 引入 guava 库包:

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

Guava Cache 使用时主要分二种模式:

  • LoadingCache 创建时需要有合理的默认方法来加载或计算与键关联的值
  • CallableCache 创建时无需关联固定的 CacheLoader 使用起来更加灵活

4.1、创建 LoadingCache 缓存

使用 CacheBuilder 来构建 LoadingCache 实例,可以链式调用多个方法来配置缓存的行为。

在创建 LoadingCache 时,需要指定 CacheLoader【理解为一个固定的加载器】,然后简单地重写 V load(K key) throws Exception 方法,就可以达到当检索不存在的时候自动加载数据的效果

public class Test {

    public static void main(String[] args) throws ExecutionException {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //设置并发级别为 8,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(8)
                // 初始化缓存容量
                .initialCapacity(10)
                // 最大缓存容量,超出就淘汰 —— 基于容量进行回收
                .maximumSize(100L)
                // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
                .recordStats()
                // 设置缓存过期时间【写入缓存后多久过期】,超过这个时间就淘汰 —— 基于时间进行回收
                .expireAfterWrite(10L, TimeUnit.SECONDS)
                // 设置缓存刷新时间【写入缓存后多久刷新缓存】,超过这个时间就刷新缓存,并调用refresh方法,默认是异步刷新
                .refreshAfterWrite(5L, TimeUnit.SECONDS)
                // key 使用弱引用 WeakReference
                .weakKeys()
                // 当 Entry 被移除时的监听器
                .removalListener(notification -> System.out.println("notification=" + notification))
                // 创建一个 CacheLoader,重写 load() 方法,以实现"当 get() 时缓存不存在,则调用 load() 方法,放到缓存并返回"的效果
                .build(new CacheLoader<String, String>() {

                    // 自动写缓存数据的方法
                    @Override
                    public String load(String key) throws Exception {
                        System.out.println("调用 load() 方法, key 为:" + key);
                        return "zzc";
                    }

                    // 异步刷新缓存
                    @Override
                    public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                        return super.reload(key, oldValue);
                    }
                });

        cache.put("name", "zzc");
        // key 为 name 时,不会调用 load() 方法,直接从缓存中获取【因为先执行了 put() 方法】
        String nameValue = cache.get("name");
        System.out.println("name=" + nameValue);
        // key 为 age 时,会调用 load() 方法
        String ageValue = cache.get("age");
        System.out.println("age=" + ageValue);
        // key 为 age 时,不会调用 load() 方法, 直接返回 Callable#call() 方法结果
        String sexValue = cache.get("sex", () -> "key 不存在");
        System.out.println("sex=" + sexValue);

        // 统计信息
        System.out.println("Cache Stats:");
        // 缓存命中次数
        System.out.println("  Hits: " + cache.stats().hitCount());
        // 缓存未命中次数
        System.out.println("  Misses: " + cache.stats().missCount());
        // 缓存加载成功率
        System.out.println("  Load Success Rate: " + cache.stats().loadSuccessCount());
        // 平均加载时间:毫秒
        System.out.println("  Average Load Time: " + cache.stats().averageLoadPenalty());
    }
}

在调用 CacheBuilder#build() 方法时,必须传递一个 CacheLoader 类型的参数,CacheLoader#load() 方法需要我们提供实现当调用 LoadingCache#get() 方法时,如果缓存不存在对应 key 的记录,则CacheLoader#load() 方法会被自动调用从外存加载数据,load() 方法的返回值会作为 key 对应的 value存储到 LoadingCache 中,并从 get() 方法返回。

CacheBuilder 是Guava 提供的一个快速构建缓存对象的工具类。CacheBuilder 类采用 builder 设计模式,它的每个方法都返回 CacheBuilder 本身,直到 build()方法被调用。
该类中提供了很多的参数设置选项,你可以设置 cache 的默认大小,并发数,存活时间,过期策略等等

当然,如果你不想指定重建策略,那么你可以使用无参的 build() 方法,它将返回 Cache类型的构建对象

4.2、创建 CallableCache 缓存

在上面的 build() 方法中是可以不用创建 CacheLoader 的,不管有没有 CacheLoader,都是支持Callable 的。Cache#get() 方法中可以指定 Callable,效果跟 CacheLoader 一样。区别就是:两者定义的时间点不一样,Callable 更加灵活,可以理解为 Callable 是对 CacheLoader 的扩展。

CallableCache的方式最大的特点在于可以在 get 的时候动态的指定 load 的数据源

public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                // 最大容量为100(基于容量进行回收)
                .maximumSize(100)
                // 配置写入后多久使缓存过期-下文会讲述
                .expireAfterWrite(3, TimeUnit.SECONDS)
                // key 使用弱引用 WeakReference
                .weakKeys()
                // 当Entry 被移除时的监听器
                .removalListener(notification -> System.out.println("notification=" + notification))
                // 不指定 CacheLoader
                .build();
        String nameValue = cache.get("name", () -> "zzc");
        System.out.println("name=" + nameValue);
        String ageValue = cache.getIfPresent("age");
        System.out.println("age=" + ageValue);
        if (ageValue == null) {
            cache.put("age", "18");
        }
        String ageValue2 = cache.getIfPresent("age");
        System.out.println("age=" + ageValue2);
        Thread.sleep(4000);
        // key 失效 或者 invalidate() 方法
        cache.invalidate("age");
        System.out.println("age=" + ageValue);
    }
}

4.3、可选配置分析

4.3.1、缓存的并发级别

Guava 提供了设置并发级别的 api,使得缓存支持并发的写入和读取。同 ConcurrentHashMap 类似Guava cache 的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器 cpu 核心数是一个比较不错的选择

  • concurrencyLevel(Runtime.getRuntime().availableProcessors())

4.3.2、缓存的初始容量设置

在构建缓存时可以为缓存设置一个合理大小初始容量,由于 Guava 的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以,合理的初始容量能够减少缓存容器的扩容次数

  • initialCapacity(100)

4.3.3、缓存失效回收策略

4.3.3.1、基于容量/权重回收

Guava Cache 可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当 Cache 中的记录数量达到最大值后再调用 put() 方法向其中添加对象,Guava 会先从当前缓存的对象记录中根据 LRU(最近最少使用)算法来移除一些条目,腾出空间后再将新的对象存储到 Cache

  • 基于容量回收maximumSize(long) 方法。设置 Cache 的最大容量数,当缓存数量达到或接近该最大值时,Cache 将清除掉那些最近最少使用的缓存
  • 基于权重回收weigher(Weigher) 方法指定一个权重、maximumWeight(long) 指定最大总重。允许根据每个条目的权重来限制缓存【缓存的条目大小不一致时特别有用】;淘汰的顺序仍然是根据条目的访问顺序,而不是权重大小
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
	// 缓存最多可以存储的总权重
    .maximumWeight(100L) 
    .weigher((key, value) -> (int) Math.ceil(instrumentation.getObjectSize(value) / 1024L))
    .build();

先计算 weight 的 value 对象的字节数,每 1kb 字节作为一个权重,对比限制缓存记录,我们就能将缓存的总占用限制在 100kb 左右

【注意】:maximumSizeweigher 不可以同时使用

【说明】:权重是在缓存创建时计算的,因此要考虑权重计算的复杂度

4.3.3.2、定时回收
  • 基于写操作的回收
    • expireAfterWrite() 方法:设置的缓存条目在给定时间内没有被更新(创建或覆盖),则会被回收。这种策略适用于当信息在一段时间后就不再有效或变得陈旧
    • refreshAfterWrite() 方法 :写入数据后多久过期【只阻塞当前数据加载线程,其它线程返回旧值】
  • 基于访问操作的回收expireAfterAccess() 方法。设置的缓存条目在给定时间内没有被读取或写入,则会被回收。这种策略适用于需要回收那些可能很长时间都不会被再次使用的条目

这几个策略时间可以单独设置,也可以组合配置

  • expireAfterAccess() 方法、expireAfterWrite() 方法:
    • 使用场景:业务非常注重缓存的时效性
    • 缺点:性能较差,缓存过期后,所有线程都要等待和锁争用,尽管 guava 可以保证只有一个线程 load 缓存(很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应),但是其它线程也要等待和锁争用
  • refreshAfterWrite() 方法
    • 优点:refresh 性能要比 load 好很多,guava 保证只有一个线程 refresh 缓存
    • 缺点:其它缓存返回旧值,这个旧值可能是很多之前的旧值(因为 refresh 动作不是自动轮询执行的,而是在 get 请求的时候才会检查是否需要 refresh。如需要 refresh,其它线程直接返回旧值可能是很久之前的,有效减少等待和锁的争用,性能较好)

【注意】:expireAfterWriteexpireAfterAccess 同时存在,以 expireAfterWrite 为准

  考虑到时效性,可以使用 expireAfterWrite(),使每次更新之后在指定的时间让缓存失效,然后重新加载缓存。guava cache 会严格限制只有1个加载 load 操作,这样会很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应
  但是,guava cache 在限制只有1个加载 load 操作时进行加锁,其它请求必须阻塞等待这个加载操作完成;而且,在加载完成之后,其它请求的线程会逐一获得锁,去判断是否已被加载完成,每个线程必须轮流地走一个“获得锁,获得值,释放锁”的过程,这样性能会有一些损耗。如果本地缓存过期时间段,频繁的过期和加载,锁等待等过程会让性能有较大的损耗

  因此,可以使用 refreshAfterWrite()refreshAfterWrite的特点:在 refresh 的过程中,严格限制只有1个重新加载操作,而其它查询先返回旧值,这样有效地可以减少锁等待和锁争用,所以refreshAfterWrite 会比 expireAfterWrite 性能好。但是它也有一个缺点,因为到达指定时间后,它不能严格保证所有的查询都获取到新值guava cache 并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行加载或刷新。所以,如果使用 refreshAfterWrite在吞吐量很低的情况下,如很长一段时间内没有查询之后,发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题

refreshAfterWriteexpireAfterWrite 两种方式各有优缺点,各有使用场景,但也可以将两者相结合:

expireAfterWrite=2s
refreshAfterWrite=1s

控制缓存每 1s 进行 refresh,如果超过 2s 没有访问,那么则让缓存失效,下次访问时不会得到旧值,而是必须得待新值加载

4.3.3.3、基于引用回收

Guava Cache 提供了基于引用的回收机制,这种机制允许缓存通过使用弱引用(weak references)或软引用(soft references)来存储键(keys)或值(values),以便 JVM 在 GC 时顺带实现缓存的清除,不过一般不轻易使用这个特性。

  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

三个方法:

  • weakKeys() 方法:使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用键的缓存用 "=="而不是 equals 比较键
  • weakValues() 方法:使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用值的缓存用 “==” 而不是 equals 比较值
  • softValues() 方法:使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用 “==” 而不是 equals 比较值

【注意】:weakValuessoftValues 不可以同时使用

  1. 基于引用的回收策略不是由缓存大小或元素的存活时间决定的,而是与 JVM 的垃圾回收机制紧密相关,而垃圾回收的行为会受到 JVM 配置和当前内存使用情况的影响,因此,引用回收策略下缓存回收具有不确定性,会导致缓存行为的不可预测性
  2. 基于引用的回收策略通常不应与需要精确控制内存占用的场景混用。在使用基于引用的回收策略时,应该仔细考虑应用程序的内存需求和垃圾回收行为,以确保缓存能够按照预期工作
4.3.3.4、显式清除
  • invalidate(key):移除单个条目
  • invalidateAll(keys):移除多个条目
  • invalidateAll():移除所有条目
  • asMap():通过缓存的 asMap() 方法获取的 ConcurrentMap 视图,你可以使用 Map 接口提供的方法来移除条目
// 移除单个条目
cache.asMap().remove(key);

// 批量移除条目
for (KeyType key : keys) {
    cache.asMap().remove(key);
}

// 移除满足特定条件的条目
cache.asMap().entrySet().removeIf(entry -> entry.getValue().equals(someValue));

asMap 视图提供了缓存的 ConcurrentMap 形式,这种方式在使用时和直接操作缓存的交互有区别,如下:

  1. cache.asMap() 包含当前所有加载到缓存的项。因此 cache.asMap().keySet() 包含当前所有已加载键;
  2. asMap().get(key) 实质上等同于 cache.getIfPresent(key),而且不会引起缓存项的加载。这和 Map 的语义约定一致。
  3. 所有读写操作都会重置相关缓存项的访问时间,包括 Cache.asMap().get(Object) 方法和 Cache.asMap().put(K, V) 方法,但不包括 Cache.asMap().containsKey(Object) 方法,也不包括在 Cache.asMap() 的集合视图上的操作。比如,遍历 Cache.asMap().entrySet() 不会重置缓存项的读取时间
  • 注册移除监听器:可以在构建缓存时注册一个移除监听器(RemovalListener),它会在每次条目被移除时调用
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
    .removalListener(new RemovalListener<KeyType, ValueType>() {
        @Override
        public void onRemoval(RemovalNotification<KeyType, ValueType> notification) {
            // 处理移除事件
        }
    })
    .build();

默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用 RemovalListeners.asynchronous(RemovalListener, Executor) 把监听器装饰为异步操作

【总结】:在实际项目实践中,往往是多种回收策略一起使用,让 Guava Cache 缓存提供多层次的回收保障

4.3.4、缓存失效回收时机

如果设置的存活时间为一分钟,应该是一分钟后这个 key 就会立即清除掉的。

如果要实现这个功能,那 Cache 中就必须存在线程来进行周期性地检查、清除等工作,很多 cache【redis、ehcache】 都是这样实现的

但是,使用 CacheBuilder 构建的缓存不会 “自动” 执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做【如果写操作实在太少的话】。这意味着,缓存可能会暂时超过最大容量限制和时间限制,直到下一次写操作触发清理
如果你需要确定缓存何时被清理,或者你想手动控制清理操作的时机可以通过「显式清除」的方式,条目删除操作会立即执行

因为如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样 CacheBuilder 就不可用了

4.3.5、自动加载

get() 方法的时候,如果 key 不存在,你可以使用指定方法去加载这个key。在 Cache 构建的时候通过指定 CacheLoder 的方式。如果你没有指定,你也可以在 get 的时候显式的调用 call() 方法来设置 key 不存在的补救策略

Cache 的 get() 方法有两个参数,第一个参数是要从 Cache 中获取记录的 key,第二个记录是一个 Callable 对象

当缓存中已经存在 key 对应的记录时,get() 方法直接返回 key 对应的记录。如果缓存中不包含 key 对应的记录,Guava 会启动一个线程执行 Callable 对象中的 call() 方法,call() 方法的返回值会作为 key 对应的值被存储到缓存中,并且被 get() 方法返回

4.3.6、统计信息

可以对 Cache 的命中率、加载数据时间等信息进行统计。在构建 Cache 对象时,可以通过CacheBuilder#recordStats()方法开启统计信息的开关。开关开启后,Cache 会自动对缓存的各种操作进行统计,调用 Cache#stats() 方法可以查看统计后的信息

4.3.7、刷新

了解了 ·Guava Cache· 的使用和回收策略后,我们会发现这种用法还存在以下两个问题:

  1. 缓存击穿。数据大批量过期会导致对后端存储的高并发访问,加载数据过程中会锁住整个 segment,很容易导致线程 block
  2. 数据不新鲜。缓存中的数据不是最新的,特别是对于那些定期变化的数据无法做到定期刷新

Guava Cache 的刷新机制允许缓存项在满足特定条件时自动刷新。这意味着缓存项的值将被重新计算和替换,但这个过程是异步的,即刷新操作不会阻塞对缓存项的读取请求。

刷新机制主要通过 LoadingCache#refresh()方法来实现,该方法会根据缓存的 CacheLoader 重新加载缓存项的值。通过 CacheBuilder#refreshAfterWrite() 方法设置自动刷新的触发条件,即在写入缓存项后的指定时间间隔。例如:

LoadingCache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
	// 在写入后的10分钟后自动刷新
    .refreshAfterWrite(10, TimeUnit.MINUTES) 
    .build(new CacheLoader<KeyType, ValueType>() {
        @Override
        public ValueType load(KeyType key) {
            // 缓存项不存在时加载数据的方法
            return loadData(key);
        }

        @Override
        public ListenableFuture<ValueType> reload(KeyType key, ValueType oldValue) throws Exception {
            // 异步刷新缓存项的方法
            // 使用ListenableFuture来异步执行刷新操作
            return listeningExecutorService.submit(() -> loadData(key));
        }
    });

4.4、Guava cache 的优劣势和适用场景

  • 优劣势:Guava cache 通过内存处理数据,具有减少 IO 请求,读写性能快的优势,但是受内存容量限制,只能处理少量数据的读写,还有可能对本机内存造成压力,并且在分布式部署中,会存在不同机器节点数据不一致的情况,即缓存漂移
  • 适用场景:读多写少,对数据一致性要求不高的场景

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部