清茶书香

一杯清茶,一本书籍,一个下午。


  • 首页

  • 归档

  • 分类

  • 关于

  • 搜索
Redis JPA Solr SpringData SpringMVC localRepository local Mapper 事务 Mybatis JDBC AOP DI IOC 常用函数 触发器 存储过程 Promise Gateway SpringCloud vue-cli axios es6 webpack npm vue 个性化 zsh 终端 caffeine jvm缓存 guava cache validation Mapping MapStruct comment 小程序 建站 WeHalo config logback plugins database idea maven spring https http nginx password RabbitMQ 秒杀系统 Windows MySQL 数据备份 halo SpringBoot shell Linux ip Optional Stream Lambda k8s Docker 列编辑 vim MacOS 图片合成 Java 远程联调 nps 内网穿透

学习JVM缓存之GuavaCache

发表于 2020-12-22 | 分类于 cache | 0 | 阅读次数 273

JVM缓存就是将数据缓存到JVM内存中,这种缓存是最快的一种缓存方式。如果缓存的数据不多,并且服务是单机的话,那么就可以考虑使用JVM缓存而非使用第三方的redis、memcache等。

JVM缓存我们可以选择将需要缓存的数据存储到一个Map中,然后启动一个线程去管理这个Map中的数据。Guava Cache做的就是这样的一件事。当然它会把这件事做好的同时拓展更多的接口,使开发者使用时更加方便。

安装依赖

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

构建缓存

普通缓存

Guava Cache提供了Builder模式构建缓存对象。

@Test
public void cacheTest() throws InterruptedException {
  Cache<String, String> cache = CacheBuilder.newBuilder()
    //设置并发级别为8,并发级别是指可以同时写缓存的线程数
    .concurrencyLevel(8)
    //设置缓存容器的初始容量为10
    .initialCapacity(10)
    //设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用算法来移除缓存项
    .maximumSize(100)
    // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
    .recordStats()
    // 设置写缓存后n秒钟过期
    .expireAfterWrite(60, TimeUnit.SECONDS)
    // 设置n秒未访问就过期
    // .expireAfterAccess(60, TimeUnit.SECONDS)
    // 写入数据后n秒过期,只阻塞当前数据加载线程,其他线程返回旧值
    // .refreshAfterWrite(60, TimeUnit.SECONDS)
    // 缓存移除监听器
    .removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
    .build();

  // 存缓存
  cache.put("test", "test");
  // 输出缓存的统计信息
  System.out.println(cache.stats().toString());
  // 获取缓存
  String test = cache.getIfPresent("test");
  // 再次查看缓存的状态(缓存状态只有在get操作后才会更新)
  System.out.println(cache.stats().toString());
  Assert.assertEquals("test", test);
  // 等待60秒,让缓存过期
  TimeUnit.SECONDS.sleep(60);
  // 还可以使用invalidate方法手动移除缓存
  // cache.invalidate("test");
  test = cache.getIfPresent("test");
  System.out.println(test);
  // 查看缓存的状态
  System.out.println(cache.stats().toString());
  Assert.assertNull(test);
}

自动加载缓存

LoadingCache

LoadingCache是Cache的一个子接口,该接口提供了一个load方法,通过这个方法可以提供一个加载缓存的方式,比如从数据库获取数据等。

public class DemoCacheLoader extends CacheLoader<String, Object> {

    @Override
    public Object load(@Nonnull String key) throws Exception {
        // 从数据库加载数据,这里模拟从数据库加载数据
        return this.loadData().get(key);
    }

    private Map<String, Object> loadData() {
        Map<String, Object> map = new LinkedHashMap<>();
        List<String> list = new LinkedList<>();
        list.add("111");
        list.add("222");
        list.add("333");
        list.add("444");
        list.add("555");
        list.add("666");
        list.add("777");
        map.put("num", list);
        map.put("test", "test");
        map.put("name", "Bennett");
        map.put("age", 24);
        map.put("sex", "man");
        return map;
    }
}
@Test
public void cacheLoaderTest() throws ExecutionException, InterruptedException {
  // 默认监听器是一个同步操作,可以使用下面的代码将其装饰成异步操作
  RemovalListener<Object, Object> listener = RemovalListeners.asynchronous(notification -> {
    System.out.printf("%s : %s 被移除了\n", notification.getKey(), notification.getValue());
  }, Executors.newSingleThreadExecutor());
  LoadingCache<String, Object> cacheLoader = CacheBuilder.newBuilder()
    // 设置并发级别为8,并发级别是指可以同时写缓存的线程数
    .concurrencyLevel(8)
    // 设置缓存容器的初始容量为10
    .initialCapacity(10)
    // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
    .maximumSize(100)
    // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
    .recordStats()
    // 设置写缓存后n秒钟过期
    .expireAfterWrite(60, TimeUnit.SECONDS)
    // 设置n秒未访问就过期
    // .expireAfterAccess(60, TimeUnit.SECONDS)
    // 写入数据后n秒过期,只阻塞当前数据加载线程,其他线程返回旧值
    // .refreshAfterWrite(60, TimeUnit.SECONDS)
    // 设置移除监听器
    .removalListener(listener)
    // build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
    .build(new DemoCacheLoader());

  String test = (String) cacheLoader.get("test");
  System.out.println(test);
  System.out.println(cacheLoader.stats().toString());
  TimeUnit.SECONDS.sleep(60);
  // 又通过load方法加载了一遍,所以会觉得好像没过期一样
  test = (String) cacheLoader.get("test");
  System.out.println(cacheLoader.stats().toString());
  System.out.println(test);
}

使用缓存默认值

@Test
public void defaultCacheTest() throws ExecutionException, InterruptedException {
  Cache<String, Object> cache = CacheBuilder.newBuilder()
    //设置并发级别为8,并发级别是指可以同时写缓存的线程数
    .concurrencyLevel(8)
    //设置缓存容器的初始容量为10
    .initialCapacity(10)
    //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
    .maximumSize(100)
    // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
    .recordStats()
    // 设置写缓存后n秒钟过期
    .expireAfterWrite(60, TimeUnit.SECONDS)
    .removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
    .build();

  String val = (String) cache.get("key1", () -> {
    System.out.println(Thread.currentThread().getName());
    return "default_value";
  });
  System.out.println(val);
}

Cache的更多方法

@Test
public void moreCacheTest() throws ExecutionException, InterruptedException {
  Cache<String, Object> cache = CacheBuilder.newBuilder()
    //设置并发级别为8,并发级别是指可以同时写缓存的线程数
    .concurrencyLevel(8)
    //设置缓存容器的初始容量为10
    .initialCapacity(10)
    //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
    .maximumSize(100)
    // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
    .recordStats()
    // 设置写缓存后n秒钟过期
    .expireAfterWrite(60, TimeUnit.SECONDS)
    // 设置key,value的弱引用/软引用
    .weakKeys()
    // .weakValues()
    .softValues()
    .removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
    .build();

  String test = new String("test");
  cache.put("test", test);
  test = new String("11111");
  System.gc();
  // 弱引用则输出null,软引用值会依旧存在(软引用只有jvm内存不足时才会将其回收)
  System.out.println(cache.getIfPresent("test"));
  
  // 手动移除缓存
  cache.invalidate("key");
  String[] keys = new String[]{"111", "222"};
  // 移除部分/全部缓存
  cache.invalidateAll(Arrays.asList(keys));
  cache.invalidateAll();
}

缓存的移除时机

redis中有key的懒惰移除,即不使用get操作就不会执行移除操作。而Guava Cache中也是类似的,缓存在未执行get操作时并不会去将缓存移除,可以通过下面的一个demo来观察效果。

@Test
    public void cacheRemoveTest() throws InterruptedException {
        Cache<String, Object> cache = CacheBuilder.newBuilder()
                //设置并发级别为8,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(8)
                //设置缓存容器的初始容量为10
                .initialCapacity(10)
                //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
                .maximumSize(100)
                // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
                .recordStats()
                // 设置写缓存后n秒钟过期
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
                .build();

        cache.put("testkey", "testval");

        new Thread(()->{
            while (true) {
                System.out.println(LocalTime.now() + ", size: " + cache.size());
                // 每隔一秒获取一次缓存大小
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ignored) {

                }
            }
        }).start();

        // 睡眠2分钟(缓存过期时间为5秒,为了保证确实应该过期,设置等待时长为2分钟,如果测试时觉得时间太长可以调短一点)
        try {
            TimeUnit.MINUTES.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("==============分隔符==============");
        cache.put("hhh", "aaaa");
        System.out.println(cache.size());

        // 睡眠2分钟(保证第二个key也过期)
        try {
            TimeUnit.MINUTES.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------------------分隔符--------------");
        System.out.println(cache.size());

        System.out.println("*****************分隔符***************");
        System.out.println("hhh:" + cache.getIfPresent("hhh") + ", cache size: " + cache.size());
    }

下面是输出的部分数据,重复的无意义数据没有全部放出来。

22:57:29.142, size: 1
22:57:30.148, size: 1
22:57:31.150, size: 1
22:57:32.151, size: 1
...
==============分隔符==============
2
22:59:29.487, size: 2
22:59:30.489, size: 2
23:01:27.819, size: 2
23:01:28.823, size: 2
...
------------------分隔符--------------
2
*****************分隔符***************
hhh:null, cache size: 1

从上面输出的信息来看,确实是如果不进行get操作就不会移除缓存,从放入第一个key后缓存大小为1,然后在2分钟后(此时第一个key已过期),放入第二个key,此时缓存大小是2。这说明2分钟后缓存依旧未移除,4分钟后第二个key的缓存也过期后,再次查看缓存大小时,缓存大小依旧2,直至get操作第二个key后,缓存大小才变为1。这说明确实是只有get操作后才会进行缓存的移除,与redis的懒惰移除是一样的,Guava Cache并未另起线程去检测缓存是否已过期,并在过期后立马将其移除。

参考文章

作者:rickiyang

出处:Guava cache使用总结

作者:poype

出处:Guava Cache用法介绍

作者:零壹技术栈

出处:理解Java的强引用、软引用、弱引用和虚引用

如果文章中有错误的,欢迎在评论区留言指正,谢谢!

Bennett wechat
欢迎收藏我的微信小程序,方便查看更新的文章。
  • 本文作者: Bennett
  • 本文链接: https://hibennett.cn/archives/study-guava-cache
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# cache # guava # jvm缓存
参数校验框架之spring-vaildation
学习JVM缓存之Caffeine
  • 文章目录
  • 站点概览
Bennett

Bennett

60 日志
28 分类
74 标签
RSS
Github E-mail Gitee QQ
Creative Commons
Links
  • MacWk
  • 知了
0%
© 2020 — 2023 hibennett.cn版权所有
由 Halo 强力驱动
|
主题 - NexT.Pisces v5.1.4

浙公网安备 33010802011246号

    |    浙ICP备2020040857号-1