JAVA本地缓存-Google Guava Cache

上接:JAVA本地缓存

介绍

Google Guava Cache是一种非常优秀本地缓存解决方案
是在 内存 中缓存数据,相比较于数据库或redis存储,访问内存中的数据会更加高效。

优点

  • 封装了 getput 操作
  • 提供线程安全的缓存操作
  • 提供过期策略
  • 提供回收策略
  • 缓存监控
  • 当缓存的数据超过最大值时,使用LRU算法替换。

应用场景

Guava官网介绍,下面的这几种情况可以考虑使用Guava Cache:

  • 愿意消耗一些内存空间来提升速度。

  • 预料到某些键会被多次查询。

  • 缓存中存放的数据总量不会超出内存容量。

所以,可以将程序 频繁用到的少量数据 存储到Guava Cache中,以改善程序性能

使用

pom.xml 依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

加载方式

Guava Cache主要有两种加载方式:

  • Cache
  • CacheLoader

Cache 缓存

Guava的缓存有许多配置选项,所以为了简化缓存的创建过程,使用了Builder设计模式

Cache 是通过 CacheBuilderbuild() 方法构建,它是Gauva提供的最基本的缓存接口

直接放入数据

将Guava Cache当作 HashMap,直接调用 put(key,value) 方法将数据放入到缓存

package std;

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

public class Std_Cache {

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

        // 将数据放入缓存
        cache.put("1", "北京");
        cache.put("2", "黑龙江");
        cache.put("3", "吉林");

        // 从缓存获取数据
        for (int i = 1; i <= 4; i++) {
            // 通过 getIfPresent() 方法取值
            System.out.println(i+"的值是:"+cache.getIfPresent(i+""));
        }
    }
}

执行结果:

1的值是:北京
2的值是:黑龙江
3的值是:吉林
4的值是:null // 缓存中没有4这条记录,所以取值是null

指定缓存值的计算逻辑(使用较少)

针对不同的key,可以分别指定缓存值的计算计算

package std;

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

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

public class Cache2_指定缓存值的计算逻辑 {

    private Cache<String, String> cache;
    public Cache2_指定缓存值的计算逻辑() {

        cache = CacheBuilder
                .newBuilder()
                .build();
    }

    public String get1() throws ExecutionException {
        // 针对不同的key,可以分别指定缓存值的计算计算
        String name = cache.get("1", new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("执行 get1 方法...");
                return "北京";
            }
        });
        return name;
    }
    public String get2() throws ExecutionException {
        // 针对不同的key,可以分别指定缓存值的计算计算
        String name = cache.get("2", new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("执行 get2 方法...");
                return "黑龙江";
            }
        });
        return name;
    }

    public static void main(String[] args) {

        Cache2_指定缓存值的计算逻辑 cache = new Cache2_指定缓存值的计算逻辑();
        try {
            System.out.println("1的值是:"+cache.get1());
            System.out.println("2的值是:"+cache.get2());

            System.out.println("1的值是:"+cache.get1());
            System.out.println("2的值是:"+cache.get2());

        } catch (ExecutionException e) {
            e.printStackTrace();
        }



    }
}

执行结果:

执行 get1 方法... // 第一次取值时,需要执行 call() 方法
1的值是:北京
执行 get2 方法... // 第一次取值时,需要执行 call() 方法
2的值是:黑龙江
1的值是:北京 // 之后再取值时,不再执行 call() 方法
2的值是:黑龙江 // 之后再取值时,不再执行 call() 方法

LoadingCache 缓存

LoadingCache 继承自 Cache,需要在 build() 方法中,指定 CacheLoader,也就是 根据 key 计算 value 的逻辑

Cache 不同,CacheLoader 作用于所有key

CacheLoader 自行管理缓存值,也就是不用 put() 放数据

例子

package std;

import com.google.common.cache.*;

public class LoadingCache0 {

    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .build(new MyCacheLoader());// 提供缓存加载器

        // 刻意获取一个不存在缓存中的key,让它去调用缓存加载器加载数据到缓存中
        for (int i = 1; i <= 4; i++) {
            try {
                System.out.println(i+"的值:::"+cache.get(String.valueOf(i)));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 再次从缓存中取值取值
        for (int i = 1; i <= 4; i++) {
            try {
                System.out.println(i+"的值:::"+cache.get(String.valueOf(i)));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {
        @Override
        public String load(String id) {
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret; // 不要返回null,会报错
        }
    };
}

执行结果:

调用缓存加载器,1 - 北京 // 第一次从缓存中取出1的值,会调用缓存加载器
1的值:::北京
调用缓存加载器,2 - 黑龙江
2的值:::黑龙江
调用缓存加载器,3 - 吉林
3的值:::吉林
调用缓存加载器,4 - // 从缓存中取出4的值,因为不存在,所以返回空字符串
4的值:::
1的值:::北京 // 之后再次从缓存中取出1的值,不再调用缓存加载器
2的值:::黑龙江
3的值:::吉林
4的值:::

取值方法

  • cache.get():先在本地缓存中取,如果不存在,则会 执行 缓存加载器。如果缓存加载器返回 null 会报错。该方法会抛出ExecutionException 异常

  • cache.getUnchecked():先在本地缓存中取,如果不存在,则会 执行 缓存加载器。如果缓存加载器返回 null 会报错。以不安全的方式获取缓存,不会抛出异常

  • cache.getIfPresent():从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回 null。一般搭配 put() 方法使用

get() 方法

在缓存加载器中,可以抛出 ExecutionException 异常,使用 get() 方法可以捕捉该异常,可以在 try...catch 处理,然后继续运行

例子

在缓存加载器中,校验参数,当id是1时,就抛出 ExecutionException 异常,使用 get() 方法可以捕捉该异常

package std;

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 LoadingCache3_取值方法get {

    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .build(new MyCacheLoader());// 提供缓存加载器


        for (int i = 1; i <= 4; i++) {
            /*
            使用 get() 方法可以捕捉 ExecutionException 异常,try...catch后,可以继续运行
             */
            try {
                System.out.println(i+"的值:::"+cache.get(i+""));
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {

        @Override
        public String load(String id) throws ExecutionException{
            /*
             校验参数,当id是1时,就抛出 ExecutionException 异常
             */
            if("1".equals(id)) {
                throw new ExecutionException("id 不能是1", null);
            }
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret;
        }
    };
}

getUnchecked() 方法

getUnchecked() 方法不会抛出异常,所以在缓存加载器中,不要抛出异常,抛出了,也无法捕捉到,也就无法处理

例子

下面代码与上面代码几乎相同,只是使用 getUnchecked() 方法取值

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 LoadingCache4_取值方法getUnchecked {

    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .build(new MyCacheLoader());// 提供缓存加载器


        for (int i = 1; i <= 4; i++) {
            /*
            使用 getUnchecked() 方法无法捕捉 ExecutionException异常,无法在try...catch处理
             */
            System.out.println(i+"的值:::"+cache.getUnchecked(i+""));
        }
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {

        @Override
        public String load(String id) throws ExecutionException{
            /*
             校验参数,当id是1时,就抛出 ExecutionException 异常
             */
            if("1".equals(id)) {
                throw new ExecutionException("测试检查异常,id="+id, null);
            }
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret;
        }
    };
}

null 值

在缓存中,不能有 null 数据,否则会报错

例子-不能将 null 放入到缓存中

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

public class Cache3_不能将null放入到缓存中 {

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

        // 将数据放入缓存
        cache.put("1", null);

    }
}

报错 java.lang.NullPointerException

例子-加载器返回null

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

public class LoadingCache1_返回null {

    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .build(new MyCacheLoader());// 提供缓存加载器

        // 刻意获取一个不存在缓存中的key,让它去调用缓存加载器加载数据到缓存中
        for (int i = 1; i <= 4; i++) {
            try {
                System.out.println(i+"的值:::"+cache.get(String.valueOf(i)));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {
        @Override
        public String load(String id) {
            String ret = null;
            if("1".equals(id)){
                ret = "北京";
            }else if("3".equals(id)) {
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret;
        }
    };
}

由于缓存加载器遇到 24 时,会返回 null,所以执行报错 CacheLoader returned null for key 2,可以用 try...catch 捕捉

缓存的初始数量

在构建缓存时可以为缓存设置一个合理大小初始容量,Guava的缓存扩容的代价非常昂贵

一般与缓存的最大容量相同

CacheBuilder.newBuilder()
        // 设置初始容量为100
        .initialCapacity(100)
        .build();

缓存淘汰机制

一般没有那么大的内存去存储 缓存数据,所以要针对那些不常用的缓存及时剔除,Guava Cache为我们提供了三种缓存剔除机制:

  • 基于缓存数据的数量剔除
  • 基于缓存数据的容量剔除
  • 基于缓存时间剔除
  • 基于引用剔除

缓存淘汰机制-缓存的最大数量

CacheBuilder.newBuilder()
        .maximumSize(100) // 设置缓存的最大容量,数据超过 该数量 后,就会按照 LRU(最近虽少使用算法)来移除缓存数据
        .build();

注意:如果 maximumSize() 传入 0,则所有key都将不进行缓存

例子

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

public class Cache4_缓存的最大容量 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .maximumSize(2) // 缓存的最大容量是2个数据
                .build();


        cache.put("1", "北京");
        cache.put("2", "黑龙江");
        // 放入第3个数据时,超过最大容量,按照 LRU(最近虽少使用算法)来移除缓存项,即:移除 1,北京
        cache.put("3", "吉林");

        // 从缓存获取数据
        for (int i = 1; i <= 3; i++) {
            // 通过 getIfPresent() 方法取值
            System.out.println(i+"的值是:"+cache.getIfPresent(i+""));
        }
    }
}

执行结果:

1的值是:null
2的值是:黑龙江
3的值是:吉林

缓存淘汰机制-缓存的最大容量

使用基于最大容量的的回收策略时,需要设置2个参数:

  • maximumWeigh:用于指定最大容量

  • weigher:在加载缓存时用于计算缓存容量大小。

当缓存的最大容量逼近或超过我们所设置的最大值时,Guava就会使用LRU算法移除缓存数据

例子

package std;

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

public class Cache5_缓存的最大容量 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .maximumWeight(10) // 设置最大容量为 10 byte
                // 设置用来计算缓存容量的 weigher
                .weigher(new Weigher<String, String>() {
                    @Override
                    public int weigh(String key, String value) {
                        int len = key.getBytes().length + value.getBytes().length;
                        System.out.println(String.format("%s-%s的字节数:%s", key,value,len));
                        return len;
                    }
                })
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        // 从缓存获取数据
        for (int i = 1; i <= 3; i++) {
            // 通过 getIfPresent() 方法取值
            System.out.println(i+"的值是:"+cache.getIfPresent(i+""));
        }
    }
}

执行结果:

1-abcd的字节数:5
2-efgh的字节数:5
3-ij的字节数:3 // 超过最大容量,根据LRU算法移除 1-abcd
1的值是:null // 被移除了,所以取值为null
2的值是:efgh
3的值是:ij

缓存淘汰机制-缓存的超时清除策略

分为2种情况

情况一:写入时间超时

写入后多长时间超时,超时后清除

Cache例子

package std;

import com.google.common.cache.*;

import java.util.concurrent.TimeUnit;

public class Cache10_写入时间超时 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .expireAfterWrite(2,TimeUnit.SECONDS) // 超时2秒就删除数据
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }
        try {
            System.out.println("休眠3秒");
            // 休眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }

    }
}

执行结果:

取值:1-abcd
取值:2-efgh
取值:3-ij
休眠3秒 // 超过2秒,所以数据被清除
取值:1-null
取值:2-null
取值:3-null

LoadingCache 例子

package std;

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

import java.util.concurrent.TimeUnit;

public class LoadingCache6_写入时间超时 {

    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .expireAfterWrite(2,TimeUnit.SECONDS) // 超时2秒就删除数据
                .build(new MyCacheLoader());

        System.out.println("第一次取值");
        for (int i = 1; i <= 3; i++) {
            String value = cache.getUnchecked(i + "");
            System.out.println("取值:" + i + "-" + value);
        }

        System.out.println("第二次取值");
        for (int i = 1; i <= 3; i++) {
            String value = cache.getUnchecked(i + "");
            System.out.println("取值:" + i + "-" + value);
        }
        try {
            System.out.println("休眠3秒");
            // 休眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第三次取值");
        for (int i = 1; i <= 3; i++) {
            String value = cache.getUnchecked(i + "");
            System.out.println("取值:" + i + "-" + value);
        }

    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {
        @Override
        public String load(String id) {
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret; // 不要返回null,会报错
        }
    };
}

执行结果:

第一次取值
调用缓存加载器,1 - 北京 // 第一次取值,执行缓存加载器
取值:1-北京
调用缓存加载器,2 - 黑龙江
取值:2-黑龙江
调用缓存加载器,3 - 吉林
取值:3-吉林
第二次取值
取值:1-北京 // 第二次取值,不执行缓存加载器
取值:2-黑龙江
取值:3-吉林
休眠3秒
第三次取值
调用缓存加载器,1 - 北京 // 第三次取值,休眠3秒,因为超时缓存数据被清除,再次执行缓存加载器
取值:1-北京
调用缓存加载器,2 - 黑龙江
取值:2-黑龙江
调用缓存加载器,3 - 吉林
取值:3-吉林

情况2:访问时间超时

每次访问后,多长时间超时,超时后清除,类似于servlet中的 session 过期时间

package std;

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

import java.util.concurrent.TimeUnit;

public class Cache11_访问时间超时 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .expireAfterAccess(2,TimeUnit.SECONDS) // 超时2秒就删除数据
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        try {
            System.out.println("休眠1秒");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }
        try {
            System.out.println("休眠1.5秒");
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }
        try {
            System.out.println("休眠3秒");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }

    }
}

执行结果:

休眠1秒
取值:1-abcd
取值:2-efgh
取值:3-ij
休眠1.5秒 // 距离上次访问间隔1.5秒,没有超时,所以能够获取到值
取值:1-abcd
取值:2-efgh
取值:3-ij
休眠3秒 // 距离上次访问间隔3秒,已经超时,数据被清除,所以获取不到值
取值:1-null
取值:2-null
取值:3-null

缓存淘汰机制-基于引用剔除

key 弱引用

Guava cache将以 弱引用 的方式去存储缓存key,那么根据弱引用的定义:当发生垃圾回收时,不管当前内存是否充足,弱引用都会被回收

package std;

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

public class Cache12_弱引用key {

    public static void main(String[] args) throws InterruptedException {
        Cache<MyKey, String> cache = CacheBuilder
                .newBuilder()
                .weakKeys() // 弱引用的方式存储key
                .build();

        // 定义key
        MyKey myKey = new MyKey("1");
        cache.put(myKey,"李雷");

        // 取出所有数据
        System.out.println(cache.asMap());
        myKey = null; // 将 myKey 置为null,变为弱引用
        System.gc(); // 垃圾回收
        Thread.sleep(2000);

        // 取出所有数据,弱引用被回收,虽然系统内存够用,但仍然回收
        System.out.println(cache.asMap());

    }

    static class MyKey{
        String key;

        public MyKey(String key) {
            this.key = key;
        }
    }

}

value 弱引用

Guava cache将以 弱引用 的方式去存储缓存value,那么根据弱引用的定义:当发生垃圾回收时,不管当前内存是否充足,弱引用都会被回收

调用方法:

CacheBuilder.weakValues()

value 软引用

Guava cache将以 软引用 的方式去存储缓存value,当内存充足时,GC不会主动回收软引用对象,当内存不足时,软引用对象就会被回收

调用方法:

CacheBuilder.softValues()

手动清除数据

在缓存构建完毕后,我们可以通过Cache提供的接口,显式的对缓存进行回收,例如:

  • Cache.invalidate(key):通过 key 清除
  • Cache.invalidateAll(keys):通过 key 批量清除
  • Cache.invalidateAll():清除所有缓存项
// 构建一个缓存
Cache<String, String> cache = CacheBuilder.newBuilder().build();

// 回收key为k1的缓存
cache.invalidate("k1");

// 将要清除的key放入到 List 中
List<String> keys = new ArrayList<>();
keys.add("k2");
keys.add("k3");
// 传入 List,实现批量清除 key
cache.invalidateAll(keys);

// 回收所有缓存
cache.invalidateAll();

缓存的并发级别

Guava提供了设置并发级别的api,使得缓存支持 并发的写入和读取
在一般情况下,将并发级别设置为服务器cpu核心数是一个比较不错的选择。

CacheBuilder.newBuilder()
        // 设置并发级别为cpu核心数
        .concurrencyLevel(Runtime.getRuntime().availableProcessors())
        .build();

定时刷新缓存

设置定时刷新的时间间隔,当达到刷新时间间隔后,再根据key获取value时,会调用缓存加载器

package std;

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

import java.util.concurrent.TimeUnit;

public class LoadingCache5_定时刷新 {

    public static void main(String[] args) throws InterruptedException {
        LoadingCache<String, String> cache = CacheBuilder
                .newBuilder()
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new MyCacheLoader());// 提供缓存加载器

        // 第一次取值时,调用缓存加载器
        String value = cache.getUnchecked("1");
        System.out.println("1的值是:"+value);

        System.out.println("休眠1秒");
        Thread.sleep(1000);
        value = cache.getUnchecked("1");
        System.out.println("1的值是:"+value);

        System.out.println("休眠1秒");
        Thread.sleep(1000);

        // 休眠2秒后,到了刷新缓存时间,取值时,再次调用缓存加载器
        value = cache.getUnchecked("1");
        System.out.println("1的值是:"+value);
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {
        @Override
        public String load(String id) {
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret; // 不要返回null,会报错
        }
    };
}

执行结果:

调用缓存加载器,1 - 北京 // 第一次取值时,调用缓存加载器
1的值是:北京
休眠1秒
1的值是:北京
休眠1秒
调用缓存加载器,1 - 北京 // 休眠2秒后,到了刷新缓存时间,取值时,再次调用缓存加载器
1的值是:北京

移除监听器

有时候希望当缓存数据被清除的时候,我们可以接收到该通知,然后可以做一些善后操作

声明 RemovalListener 监听器,当缓存数据被移除时,RemovalListener 可以监听到,可以获取移除的 key 、value、原因(RemovalCause 枚举类型),也可以可以自定义操作。

例子

import com.google.common.cache.*;

public class Cache6_移除监听器 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .maximumSize(2) // 最大缓存2个数据
                .removalListener(new MyRemovalListener())
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        /*
        由于最大缓存2个数据,放入第3个数据时,根据lru算法移除key是1的数据
        移除类型是SIZE
         */
        cache.put("3", "ij");
        /*
        手动移除key是2的数据
        移除类型是 EXPLICIT
         */
        cache.invalidate("2");
    }
    static class MyRemovalListener implements RemovalListener<String,String>{

        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            String key = notification.getKey();
            String value = notification.getValue();
            RemovalCause cause = notification.getCause();
            System.out.println("监听到移除数据:"+key+"-"+value+",原因是:"+cause);
        }
    }
}

执行结果:

监听到移除数据:1-abcd,原因是:SIZE
监听到移除数据:2-efgh,原因是:EXPLICIT

监听不到超时移除

Guava Cache 不主动 清除 超时的缓存数据,如果此时去访问了这个 Key,会检测是不是已经过期,过期就删除它。
但是如果不做任何操作,超时后也许这个 Key 还在内存中,所以监听不到

GuavaCache 选择这样做的原因也很简单,如下:

The reason for this is as follows: if we wanted to perform Cache maintenance continuously, we would need to create a thread, and its operations would be competing with user operations for shared locks. Additionally, some environments restrict the creation of threads, which would make CacheBuilder unusable in that environment.

这样做既可以保证对 Key 读写的正确性,也可以节省资源,减少竞争。

例子

package std;

import com.google.common.cache.*;

import java.util.concurrent.TimeUnit;

public class Cache7_移除监听器_监听不到超时移除 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .expireAfterWrite(2,TimeUnit.SECONDS) // 超时2秒就删除数据
                .removalListener(new MyRemovalListener())
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        try {
            // 休眠4秒
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 1; i <= 3; i++) {
            String value = cache.getIfPresent(i + "");
            System.out.println("取值:" + i + "-" + value);
        }

    }
    static class MyRemovalListener implements RemovalListener<String,String>{

        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            String key = notification.getKey();
            String value = notification.getValue();
            RemovalCause cause = notification.getCause();
            System.out.println("监听到移除数据:"+key+"-"+value+",原因是:"+cause);
        }
    }
}

执行结果:

取值:1-null // 超时后访问,返回null,但移除监听器没有监听到
取值:2-null
取值:3-null

移除监听器执行耗时操作

默认情况下,监听器方法是在移除缓存时同步调用的(在移除缓存的那个线程中执行)。如果监听器方法比较耗时,会导致调用者线程阻塞时间变长,会拖慢正常的缓存请求

例子

package std;

import com.google.common.cache.*;

import java.time.LocalTime;

public class Cache8_移除监听器_耗时操作 {

    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .removalListener(new MyRemovalListener())
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        System.out.println("手动移除key是1的数据,"+ LocalTime.now());
        cache.invalidate("1");

        // 由于移除监听器需要5s执行完,导致下面取值操作都需要等待
        String value = cache.getIfPresent("2");
        System.out.println("2的value值是:"+value+","+ LocalTime.now());

    }
    static class MyRemovalListener implements RemovalListener<String,String>{

        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String key = notification.getKey();
            String value = notification.getValue();
            RemovalCause cause = notification.getCause();
            System.out.println("监听到移除数据:"+key+"-"+value+",原因是:"+cause+","+ LocalTime.now());
        }
    }
}

执行结果:

手动移除key是1的数据,14:04:57.096
监听到移除数据:1-abcd,原因是:EXPLICIT,14:05:02.112
2的value值是:efgh,14:05:02.112 // 由于移除监听器需要5s执行完,导致后面取值操作都需要等待

解决

可以使用

RemovalListeners.asynchronous(RemovalListener, Executor)

把监听器装饰为异步操作。

例子

package std;

import com.google.common.cache.*;

import java.time.LocalTime;
import java.util.concurrent.Executors;

public class Cache9_移除监听器_耗时操作解决 {

    public static void main(String[] args) {

        RemovalListener<String, String> async = RemovalListeners.asynchronous(new MyRemovalListener(), Executors.newSingleThreadExecutor());

        Cache<String, String> cache = CacheBuilder
                .newBuilder()
                .removalListener(async)
                .build();


        cache.put("1", "abcd");
        cache.put("2", "efgh");
        cache.put("3", "ij");

        System.out.println("手动移除key是1的数据,"+ LocalTime.now());
        cache.invalidate("1");

        // 由于移除监听器需要5s执行完,导致下面取值操作都需要等待
        String value = cache.getIfPresent("2");
        System.out.println("2的value值是:"+value+","+ LocalTime.now());

    }
    static class MyRemovalListener implements RemovalListener<String,String>{

        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String key = notification.getKey();
            String value = notification.getValue();
            RemovalCause cause = notification.getCause();
            System.out.println("监听到移除数据:"+key+"-"+value+",原因是:"+cause+","+ LocalTime.now());
        }
    }
}

执行结果:

手动移除key是1的数据,14:07:55.809
2的value值是:efgh,14:07:55.820 // 2. 后面取值操作不受 移除监听器的操作影响
监听到移除数据:1-abcd,原因是:EXPLICIT,14:08:00.822 // 1. 移除监听器需要5s执行完,在另一个线程中执行

使用模板

package std;

import com.google.common.cache.*;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class LoadingCache模板 {

    public static void main(String[] args) throws ExecutionException {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                // 缓存的初始数量
                .initialCapacity(1000)
                // 设置缓存最大数量
                .maximumSize(1000)
                // 放入数据后,超过10分钟,就清除该数据
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // 并行级别
                .concurrencyLevel(Runtime.getRuntime().availableProcessors())
                // 提供缓存加载器
                .build(new MyCacheLoader());


        // 刻意获取一个不存在缓存中的key,让它去调用缓存加载器加载数据到缓存中
        for (int i = 1; i <= 4; i++) {
            System.out.println( i+"取值:::"+cache.get(i+""));
        }
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    static class MyCacheLoader extends CacheLoader<String, String> {
        @Override
        public String load(String id) {
            String ret = "";
            if("1".equals(id)){
                ret = "北京";
            }else if("2".equals(id)){
                ret = "黑龙江";
            }else if("3".equals(id)){
                ret = "吉林";
            }
            System.out.println(String.format("调用缓存加载器,%s - %s", id,ret));
            return ret; // 不要返回null,会报错
        }
    };

}

参考:
https://www.jianshu.com/p/38bd5f1cf2f2
https://www.cnblogs.com/fnlingnzb-learner/p/11022152.html
https://juejin.cn/post/7014459433077964808
https://blog.csdn.net/zhaobao1987/article/details/77799024
https://blog.csdn.net/sunyufeng22/article/details/121614900
https://www.jianshu.com/p/d0d27cf44162
https://blog.csdn.net/chen_kkw/article/details/81144169
https://blog.csdn.net/aitangyong/article/details/53127605


原文出处:https://www.malaoshi.top/show_1IX4mscf2Mjt.html