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

Guava Cache原理及最佳实践

  • [1. Guava Cache是什么](#1. Guava Cache是什么)
    • [1.1 简介](#1.1 简介)
    • [1.2 核心功能](#1.2 核心功能)
    • [1.3 适用场景](#1.3 适用场景)
  • [2. Guava Cache的使用](#2. Guava Cache的使用)
    • [2.1 创建LoadingCache缓存](#2.1 创建LoadingCache缓存)
    • [2.2 创建CallableCache缓存](#2.2 创建CallableCache缓存)

缓存的种类有很多,需要根据不同的应用场景来选择不同的cache,比如分布式缓存如redis、memcached,还有本地(进程内)缓存如:ehcache、GuavaCache、Caffeine。本篇主要围绕全内存缓存-Guava Cache做一些详细的讲解和分析。

1. Guava Cache是什么

1.1 简介

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

"automatic loading of entries into the cache"

即 :当cache中不存在要查找的entry的时候,它会自动执行用户自定义的加载逻辑,加载成功后再将entry存入缓存并返回给用户未过期的entry,如果不存在或者已过期,则需要load,同时为防止多线程并发下重复加载,需要先锁定,获得加载资格的线程(获得锁的线程)创建一个LoadingValueRefrerence并放入map中,其他线程等待结果返回。

1.2 核心功能

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReferenceSoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。

小结:

Guava Cache说简单点就是一个支持LRU的ConcurrentHashMap,并提供了基于容量,时间和引用的缓存回收方式。(简单概括)

1.3 适用场景

  • 愿意消耗一些内存空间来提升速度(以空间换时间,提升处理速度);
    • 能够预计某些key会被查询一次以上;
    • 缓存中存放的数据总量不会超出内存容量(Guava Cache是单个应用运行时的本地缓存)。
  • 计数器(如可以利用基于时间的过期机制作为限流计数)

2. Guava Cache的使用

GuavaCache使用时主要分二种模式:LoadingCacheCallableCache

核心区别在于:LoadingCache创建时需要有合理的默认方法来加载或计算与键关联的值,CallableCache创建时无需关联固定的CacheLoader使用起来更加灵活。

前置准备:

java 复制代码
// RPC调用方法,用于获取数据
private static List<String> rpcCall(String cityId) {
    // 模仿从数据库中取数据
    try {
        switch (cityId) {
            case "0101":
	            System.out.println("load cityId:" + cityId);
                return ImmutableList.of("上海", "北京", "广州", "深圳");
        }
    } catch (Exception e) {
        // 记日志
    }
    return Collections.EMPTY_LIST;
}

2.1 创建LoadingCache缓存

使用CacheBuilder来构建LoadingCache实例,可以链式调用多个方法来配置缓存的行为。其中CacheLoader可以理解为一个固定的加载器,在创建LoadingCache时指定,然后简单地重写V load(K key) throws Exception方法,就可以达到当检索不存在的时候自动加载数据的效果。

java 复制代码
//创建一个LoadingCache,并可以进行一些简单的缓存配置
private static LoadingCache<String, Optional<List<String>> > loadingCache = CacheBuilder.newBuilder()
    //配置最大容量为100,基于容量进行回收
    .maximumSize(100)
    //配置写入后多久使缓存过期-下文会讲述
    .expireAfterWrite(3, TimeUnit.SECONDS)
    //配置写入后多久刷新缓存-下文会讲述
    .refreshAfterWrite(3, TimeUnit.SECONDS)
    //key使用弱引用-WeakReference
    .weakKeys()
    //当Entry被移除时的监听器
    .removalListener(notification -> System.out.println("notification=" + notification))
    //创建一个CacheLoader,重写load方法,以实现"当get时缓存不存在,则load,放到缓存并返回的效果
    .build(new CacheLoader<String, Optional<List<String>>>() {
        //重点,自动写缓存数据的方法,必须要实现
        @Override
        public Optional<List<String>> load(String cityId) throws Exception {
            return Optional.ofNullable(rpcCall(cityId));
        }
        //异步刷新缓存-下文会讲述
        @Override
        public ListenableFuture<Optional<List<String>>> reload(String cityId, Optional<List<String>> oldValue) throws Exception {
            return super.reload(cityId, oldValue);
        }
    });

// 测试
public static void main(String[] args) {

    try {
        System.out.println("load from cache once : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
        Thread.sleep(4000);
        System.out.println("load from cache two : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
        Thread.sleep(2000);
        System.out.println("load from cache three : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
        Thread.sleep(2000);
        System.out.println("load not exist key from cache : " + loadingCache.get("0103").orElse(Lists.newArrayList()));

    } catch (ExecutionException | InterruptedException e) {
        //记录日志
    }
}

执行结果

2.2 创建CallableCache缓存

在上面的build方法中是可以不用创建CacheLoader的,不管有没有CacheLoader,都是支持Callable的。Callable在get时可以指定,效果跟CacheLoader一样,区别就是两者定义的时间点不一样,Callable更加灵活,可以理解为Callable是对CacheLoader的扩展。CallableCache的方式最大的特点在于可以在get的时候动态的指定load的数据源

java 复制代码
//创建一个callableCache,并可以进行一些简单的缓存配置
private static Cache<String, Optional<List<String>>> callableCache = CacheBuilder.newBuilder()
    //最大容量为100(基于容量进行回收)
    .maximumSize(100)
    //配置写入后多久使缓存过期-下文会讲述
    .expireAfterWrite(3, TimeUnit.SECONDS)
    //key使用弱引用-WeakReference
    .weakKeys()
    //当Entry被移除时的监听器
    .removalListener(notification -> System.out.println("notification=" + notification))
    //不指定CacheLoader
    .build();

// 测试
public static void main(String[] args) {
    try {
        System.out.println("load from callableCache once : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
        Thread.sleep(4000);
        System.out.println("load from callableCache two : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
        Thread.sleep(2000);
        System.out.println("load from callableCache three : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
        Thread.sleep(2000);
        System.out.println("load not exist key from callableCache : " + callableCache.get("0103", () -> Optional.ofNullable(rpcCall("0103"))).orElse(Lists.newArrayList()));
    } catch (ExecutionException | InterruptedException e) {
        //记录日志
    }
}

执行结果:

后续待补充

相关推荐
一直都在5723 小时前
Redis (一)
数据库·redis·缓存
秦jh_3 小时前
【Redis】客户端使用
数据库·redis·缓存
随风,奔跑4 小时前
Redis
数据库·redis·缓存
TlYf NTLE5 小时前
redis分页查询
数据库·redis·缓存
大萌神Nagato6 小时前
力扣HOT100 Q146LRU缓存
算法·leetcode·缓存
鬼蛟7 小时前
Redis
数据库·redis·缓存
8Qi88 小时前
Redis哨兵模式(Sentinel)深度解析
java·数据库·redis·分布式·缓存·sentinel
CDN3609 小时前
CDN 缓存命中率低如何提升?忽略参数与规则设置教程
运维·缓存
M--Y9 小时前
初识Redis
数据库·redis·缓存
皙然10 小时前
Redis核心理论:数据删除与淘汰策略详解(从原理到实战)
数据库·redis·缓存