美文网首页
02 初识缓存-GuavaCache

02 初识缓存-GuavaCache

作者: 花神子 | 来源:发表于2019-05-30 14:45 被阅读0次

    在上篇文章01 初识缓存-了解缓存中简单了介绍了下缓存的历程以及几种常见的技术进行简单介绍,本着学习的目的本节针对GuavaCache进行一个专题介绍,可能理解有限欢迎指正。

    一 简介

    Guava Cache,是Google 出品的 Java 核心增强库的缓存部分,一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。guava cache可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。

    二 GuavaCache使用

    2.1 缓存加载器 CacheLoader

    CacheLoader是GuavaCache提供的一种加载缓存的机制,可以通过指定的键值进行计算/加载需要进行匹配的缓存数据。GuavaCache虽然提供了这种方式,但是它并不要求编程者一定需要遵循它这种方案,也可以根据自定的实际需求来定制自己加载逻辑。根据GuavaCache提出的如果想复写其缓存的加载方式,但是仍要保留“get-if-absent-compute”语义,GuavaCache在进行缓存获取的时候提供了一种解决方案:可以在调用get方法时传入一个Callable实例,来达到目的。缓存的对象可以通过Cache.put直接插入,但是自动加载是首选,因为自动加载可以更加容易的判断所有缓存信息的一致性。

    Guava Cache针对开发者提供了非常友好的编程方式,使用非常简单:

    下面例子分别从不同的角度:

    From a CacheLoader

    LoadingCache 缓存是通过一个CacheLoader来构建缓存。创建一个CacheLoader仅需要实现V load(K key) throws Exception方法即可。通过方法get(K)可以对LoadingCache进行查询。该方法要不返回已缓存的值,要不通过CacheLoader来自动加载相应的值到缓存中。

    From a Callable

    所有类型的Guava Cache,不论是否会自动加载,都支持get(K, Callable(V))方法。当给定键的缓存值已存在时则直接返回,否则通过指定的Callable方法进行计算并将值存放到缓存中。直到加载完成时,相应的缓存才会被更改。该方法简单实现了"if cached, return; otherwise create, cache and return"("如果有缓存则返回;否则运算、缓存、然后返回")语义。

    Cache.put

    使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 应该总是优先使用。

    /**
     * @author mzw
     * @version V1.0.0
     * @description
     * @data 2019-05-29 14:53
     * @see
     **/
    public class GuavaCache {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCache.class);
    
        public static void main(String[] args) throws ExecutionException {
    
            //构建缓存加载器
            CacheLoader<String, String> loader = new CacheLoader<String, String>() {
                @Override
                public String load(String key) {
                    LOGGER.info(key + " is loaded from a cacheLoader!");
                    return key;
                }
            };
            //构建移除监听器:非必要
            RemovalListener<String, String> removalListener =
                    removal -> LOGGER.info("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");
    
            //构建缓存
            LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(4)
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .removalListener(removalListener)
                    .build(loader);
    
            //直接放入数据
            for (int i = 0; i < 3; i++) {
                cache.put("key" + i, "value" + i);
            }
            //获取数据
            LOGGER.info("key2  > " + cache.getIfPresent("key2"));
            LOGGER.info("key  > " + cache.getIfPresent("key"));
            //From a CacheLoade
            LOGGER.info("key  > " + cache.get("key"));
            //From a Callable
            LOGGER.info("key  > " + cache.get("k1", () -> "dd"));
        }
    }
    

    输出结果

    17:36:12.084 [main] INFO com.maozw.quartz.cache.GuavaCache - key2  > value2
    17:36:12.086 [main] INFO com.maozw.quartz.cache.GuavaCache - key  > null
    17:36:12.090 [main] INFO com.maozw.quartz.cache.GuavaCache - key is loaded from a cacheLoader!
    17:36:12.092 [main] INFO com.maozw.quartz.cache.GuavaCache - key  > key
    17:36:12.094 [main] INFO com.maozw.quartz.cache.GuavaCache - [key0:value0] is evicted!
    17:36:12.094 [main] INFO com.maozw.quartz.cache.GuavaCache - k1  > dd
    

    2.1 缓存回收

    guava中数据的移除分为被动移除和主动移除两种,
    被动移除数据的方式,简介中曾说过guava cache可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。Guava Cache提供了三种基本的缓存回收方式:

    基于缓存容量大小的移除

    当缓存中的元素数量超过指定值时,会把不常用的键值对从cache中移除。

    • 缓存容量大小指的是cache中的条目数;

    • 并不是完全到了缓存容量大小才开始移除不常用的数据的,而是接近缓存容量大小的时候系统就会开始做移除的动作;

    • 如果一个键值对已经从缓存中被移除了,你再次请求访问的时候,如果cachebuild是使用cacheloader方式的,那依然还是会从cacheloader中再取一次值,如果还没有,就会抛出异常。

    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(4)//缓存容量大小
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .removalListener(removalListener)
                    .build(loader);
    

    基于缓存时间的移除

    guava提供了两个基于时间移除的方法

    • expireAfterAccess(long, TimeUnit):这个方法是根据某个键值对最后一次访问之后多少时间后移除。缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。

    • expireAfterWrite(long, TimeUnit):这个方法是根据某个键值对被创建或值被替换后多少时间移除。缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的.

    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(4)//缓存容量大小
                    .expireAfterWrite(10, TimeUnit.MINUTES)//缓存时间
                    .removalListener(removalListener)
                    .build(loader);
    

    基于引用回收(Reference-based Eviction)

    这种移除方式主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除

    • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。

    • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。

    • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。

    主动移除数据方式

    主动移除有三种方法:

    • 单独缓存项移除 Cache.invalidate(key)

    • 批量缓存项移除 Cache.invalidateAll(keys)

    • 清除所有缓存项 Cache.invalidateAll()

    移除监听器

    通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。如果需要改成异步形式,可以考虑使用RemovalListeners.asynchronous(RemovalListener, Executor) 。

    缓存储移除的时机

    Guava cache中通过CacheBuilder构建的缓存数据不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,它会在读/写操作后同步进行清理工作,只是读操作时可能执行的机会会少少一些。
    原因:如果自动清理缓存,就必须存在一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。

    同时用户可以自主的去控制清除时机,比如固定的时间间隔调用Cache.cleanUp()。

    2.2 刷新

    刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃。

    重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。
    示例如下:

    /**
     * @author mzw
     * @version V1.0.0
     * @description
     * @data 2019-05-29 14:53
     * @see
     **/
    public class GuavaCacheDemo {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCacheDemo.class);
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            //构建移除监听器:非必要
            RemovalListener<String, String> removalListener =
                    removal -> LOGGER.info("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");
    
            //构建缓存
            LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(5)
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .removalListener(removalListener)
                    .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String s) {
                            return s + " -> load";
                        }
    
                        @Override
                        public ListenableFuture<String> reload(String key, String oldValue) {
                            LOGGER.info(key + "  > reload : " + oldValue);
                            ListenableFutureTask<String> reloadTask = ListenableFutureTask.create(() -> oldValue + " -> reload");
                            Executors.newFixedThreadPool(1).submit(reloadTask);
                            return reloadTask;
                        }
                    });
    
            //放入数据
            for (int i = 0; i < 3; i++) {
                cache.put("key" + i, "value" + i);
            }
            //获取数据
            LOGGER.info("key  > " + cache.getIfPresent("key"));
            LOGGER.info("key1  > " + cache.getIfPresent("key1"));
            cache.refresh("key1");
            LOGGER.info("refresh : k  > " + cache.get("key1"));
    
        }
    }
    

    输出结果

    19:32:01.069 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key  > null
    19:32:01.071 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key1  > value1
    19:32:01.077 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - k  > aa
    19:32:01.077 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key1  > reload : value1
    19:32:01.085 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - [key1:value1] is evicted!
    19:32:01.086 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - refresh : k  > value1 -> reload
    

    统计

    简单介绍
    CacheBuilder.recordStats():用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheS tats 对象以提供如下统计信息:
    hitRate():缓存命中率;
    averageLoadPenalty():加载新值的平均时间,单位为纳秒;
    evictionCount():缓存项被回收的总数,不包括显式清除。

    三 Guava Cache 解读

    在解读之前,先提一下Guava Cache的一些组件:
    Guava Cache组件中核心的类和接口列举如下:
    接口:

    • Cache 顶级接口,定义get、put、invalidate等操作,以及获取缓存统计数据的方法等。
    • LoadingCache 继承自Cache,并另外提供了一些当get数据不存在时自动去load相关key(s)所对应的value(s)的契约(即接口中的抽象方法),具体实现见LoadingCache的具体实现类。
    • RemovalListener 监听器接口,在缓存被移除的时候用来做一些操作,与下面的RemovalNotification、RemovalCause配套使用。很明显这是个观察者模式的应用。
    • Weigher 权重的接口,提供int weigh(K key, V value)抽象方法,给缓存中的Entry赋予权重信息。

    抽象类:

    • AbstractCache 本身是抽象类,实现自Cache接口,基本没做什么实际的工作,大多数方法的实现只是简单抛出UnsupportedOperationException.该抽象类提供了Cache接口的骨架,为了避免子类直接继承Cache接口时必须实现所有抽象方法,这种手法在其他地方也很常见,个人觉得都算得上是一种设计模式了。
    • AbstractLoadingCache 继承自AbstractCache并实现了LoadingCache接口,目的也是提供一个骨架,其中的某些方法提供了在get不到数据时会自动Load数据的契约。
    • CacheLoader 抽象类,最核心的方法就是load,封装load数据的操作,具体如何laod与该抽象类的具体子类有关,只需要重写laod方法,就可以在get不到数据时自动去load数据到缓存中。
    • ForwardingCache 装饰器模式的用法,所有对缓存的操作都委托给其他的实现了Cache接口的类,该抽象类中有一个抽象方法protected abstract Cache<K, V> delegate();不难推测出来,其他的方法中均使用了该代理。即类似get(key){delegate().get(key)}
    • ForwardingLoadingCache 自行推断,不解释。

    实现类:

    • CacheBuilder 建造者模式的应用,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。通过该类来组装Cache,最后调用build方法来生成Cache的实例。
    • CacheBuilderSpec 用来构建CacheBuilder的实例,其中提供了一些配置参数,用这些配置的参数来通过CacheBuilder实例最终构建Cache实例。
    • CacheStats 缓存使用情况统计信息,比如命中多少次,缺失多少次等等。
    • LocalCache 本地缓存最核心的类,Cache接口实例的代理人,Cache接口提供的一些方法内部均采委托给LocalCache实例来实现,LocalCache的具体实现类似于ConcurrentHashMap,也采用了分段的方式。

    CacheBuilder

    CacheBuilder是缓存配置和构建入口。

    在上面例子中我们构建缓存使用如下方式:现在解析CacheBuilder这个建造者的结构

    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(4)//缓存容量大小
                    .expireAfterWrite(10, TimeUnit.MINUTES)//缓存时间
                    .removalListener(removalListener)
                    .build(loader);
    

    整个类内容太长,分段进行解说:

    CacheBuilder 属性

    • DEFAULT_INITIAL_CAPACITY://缓存的默认初始化大小;

    • DEFAULT_CONCURRENCY_LEVEL:LocalCache默认并发数,用来评估Segment的个数;

    • DEFAULT_EXPIRATION_NANOS://默认的缓存过期时间;

    • initialCapacity://初始缓存大小

    • concurrencyLevel:用于计算有并发量

    • maximumSize:cache中最多能存放的缓存entry个数

    • expireAfterWriteNanos://缓存超时时间(起点:缓存被创建或被修改)

    • expireAfterAccessNanos://缓存超时时间(起点:缓存被创建或被修改或被访问)

    public final class CacheBuilder<K, V> {
      private static final int DEFAULT_INITIAL_CAPACITY = 16;
      private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
      private static final int DEFAULT_EXPIRATION_NANOS = 0;
      private static final int DEFAULT_REFRESH_NANOS = 0;
    
      static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance(
          new StatsCounter() {
            ...//省去无关代码
          });
      static final CacheStats EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0);
    
      static final Supplier<StatsCounter> CACHE_STATS_COUNTER =
          new Supplier<StatsCounter>() {
        @Override
        public StatsCounter get() {
          return new SimpleStatsCounter();
        }
      };
    
        ...//省去无关代码
    
      private static final Logger logger = Logger.getLogger(CacheBuilder.class.getName());
    
      static final int UNSET_INT = -1;
    
      boolean strictParsing = true;
    
      int initialCapacity = UNSET_INT;
      int concurrencyLevel = UNSET_INT;
      long maximumSize = UNSET_INT;
      long maximumWeight = UNSET_INT;
      Weigher<? super K, ? super V> weigher;
    
      Strength keyStrength;//键的引用类型(strong、weak、soft)
      Strength valueStrength;//值的引用类型(strong、weak、soft)
    
      long expireAfterWriteNanos = UNSET_INT;
      long expireAfterAccessNanos = UNSET_INT;
      long refreshNanos = UNSET_INT;
      //key比较策略 
      Equivalence<Object> keyEquivalence;
      Equivalence<Object> valueEquivalence;
    
      RemovalListener<? super K, ? super V> removalListener;//元素被移除的监听器
      Ticker ticker;
      //状态计数器,默认为NULL_STATS_COUNTER,即不启动计数功能
      Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER;
    
        ...
    }
    

    build方法

    CacheBuilder构建缓存有两个方法:

    • 构建一个具有数据加载功能的缓存,调用LocalCache构造方法

    • 构建一个没有数据加载功能的缓存,调用LocalCache构造方法,但loader为null

    public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
          CacheLoader<? super K1, V1> loader) {
        checkWeightWithWeigher();
        return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
    }
    
    public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
        checkWeightWithWeigher();
        checkNonLoadingCache();
        return new LocalCache.LocalManualCache<K1, V1>(this);
    }
    

    至此我们就需要先了解一下LocalCache了,因为我们此时调用该类的构造方法LocalCache()和实例方法LocalLoadingCache()/LocalManualCache()

    LocalCache

    LocalCache是guava cache的核心类。

    LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各segment相对独立,互不影响,所以能支持并行操作。每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray<ReferenceEntry<K, V>>,即一个数组,数组中每个元素是一个链表。两个队列分别是writeQueue和accessQueue,用来存储写入的数据和最近访问的数据,当数据过期,需要刷新整体缓存(见上述示例最后一次cache.getIfPresent("key5"))时,遍历队列,如果数据过期,则从table中删除。

    LocalCache 数据结构

    Segment<K, V>[] segments;

    Segment继承于ReetrantLock,减小锁粒度,提高并发效率。

    AtomicReferenceArray<ReferenceEntry<K, V>> table;

    类似于HasmMap中的table一样,相当于entry的容器。

    ReferenceEntry<K, V> referenceEntry;

    基于引用的Entry,其实现类有弱引用Entry,强引用Entry等

    ReferenceQueue<K> keyReferenceQueue;

    已经被GC,需要内部清理的键引用队列。

    ReferenceQueue<V> valueReferenceQueue;

    已经被GC,需要内部清理的值引用队列。

    Queue<ReferenceEntry<K, V>> recencyQueue;

    记录升级可访问列表清单时的entries,当segment上达到临界值或发生写操作时该队列会被清空。

    Queue<ReferenceEntry<K, V>> writeQueue;

    按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。

    Queue<ReferenceEntry<K, V>> accessQueue;

    按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部

    LocalCache 构造器

    构造器是通过CacheBuilder的方法对变量进行初始化。具体变量解说可参照CacheBuilder 属性解说。

      /**
       * Creates a new, empty map with the specified strategy, initial capacity and concurrency level.
       */
      LocalCache(
          CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
        concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
        //默认为强引用
        keyStrength = builder.getKeyStrength();
        valueStrength = builder.getValueStrength();
    
        keyEquivalence = builder.getKeyEquivalence();
        valueEquivalence = builder.getValueEquivalence();
    
        maxWeight = builder.getMaximumWeight();
        weigher = builder.getWeigher();
        expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
        expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
        refreshNanos = builder.getRefreshNanos();
    
        removalListener = builder.getRemovalListener();
        removalNotificationQueue = (removalListener == NullListener.INSTANCE)
            ? LocalCache.<RemovalNotification<K, V>>discardingQueue()
            : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();
    
        ticker = builder.getTicker(recordsTime());
        entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
        globalStatsCounter = builder.getStatsCounterSupplier().get();
        defaultLoader = loader;
    
        int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
        if (evictsBySize() && !customWeigher()) {
          initialCapacity = Math.min(initialCapacity, (int) maxWeight);
        }
         ...
        int segmentShift = 0;
        int segmentCount = 1;
        while (segmentCount < concurrencyLevel
               && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
          ++segmentShift;
          segmentCount <<= 1;
        }
        this.segmentShift = 32 - segmentShift;
        segmentMask = segmentCount - 1;
        //初始化segments大小
        this.segments = newSegmentArray(segmentCount);
    
        int segmentCapacity = initialCapacity / segmentCount;
        if (segmentCapacity * segmentCount < initialCapacity) {
          ++segmentCapacity;
        }
    
        int segmentSize = 1;
        while (segmentSize < segmentCapacity) {
          segmentSize <<= 1;
        }
        //初始化Segments
        if (evictsBySize()) {
          // Ensure sum of segment max weights = overall max weights
          long maxSegmentWeight = maxWeight / segmentCount + 1;
          long remainder = maxWeight % segmentCount;
          for (int i = 0; i < this.segments.length; ++i) {
            if (i == remainder) {
              maxSegmentWeight--;
            }
            this.segments[i] =
                createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
          }
        } else {
          for (int i = 0; i < this.segments.length; ++i) {
            this.segments[i] =
                createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
          }
        }
      }
    

    Segment初始化操作

    初始化容器

    Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight,
            StatsCounter statsCounter) {
          this.map = map;
          this.maxSegmentWeight = maxSegmentWeight;
          this.statsCounter = checkNotNull(statsCounter);
          initTable(newEntryArray(initialCapacity));//初始化table
    
          keyReferenceQueue = map.usesKeyReferences()
               ? new ReferenceQueue<K>() : null;//key引用队列
    
          valueReferenceQueue = map.usesValueReferences()
               ? new ReferenceQueue<V>() : null;//value引用队列
    
          recencyQueue = map.usesAccessQueue()
              ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
    
          writeQueue = map.usesWriteQueue()
              ? new WriteQueue<K, V>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();//写入元素队列
    
          accessQueue = map.usesAccessQueue()
              ? new AccessQueue<K, V>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();//访问元素队列
    }
    

    以上是整个初始化流程


    LocalCache put加载缓存

    Cache 接口声明

    @Beta
    @GwtCompatible
    public interface Cache<K, V> {
       void put(K key, V value);
    }
    

    LocalCache的实现

    • 内部调用segmentFor(int hash)方法,该方法返回指定的散列hash键值匹配的段。

    • 然后调用Segment的put方法

    @GwtCompatible(emulated = true)
    class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
    @Override
      public V put(K key, V value) {
        checkNotNull(key);
        checkNotNull(value);
        int hash = hash(key);
        return segmentFor(hash).put(key, hash, value, false);
      }
    }
    

    segmentFor(hash).put(key, hash, value, false)

    下面第四行代码可以看出preWriteCleanup在每次put之前都会清理动作,我在缓存储移除的时机小段中进行过提示,缓存的清除时机是在读/写操作的时候进行的。

    @Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
      lock();
      try {
        long now = map.ticker.read();
        preWriteCleanup(now);
    
        int newCount = this.count + 1;//localCache的Count+1
        if (newCount > this.threshold) { // ensure capacity 是否要进行扩容
          expand();//扩容
          newCount = this.count + 1;
        }
        //获取当前Entry中的HashTable的Entry数组
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);//计算散列值对应table的索引位置
        ReferenceEntry<K, V> first = table.get(index);//通过索引获取ReferenceEntry
    
        // Look for an existing entry.进行遍历 如果找到则进行下面逻辑
        for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            // We found an existing entry.如果找到则进行下面逻辑
            // 对应的值引用
            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();//获取值
            // cache 提供基于引用的回收策略,此处可能为null:即可能会GC了
            if (entryValue == null) {
              ++modCount;
              if (valueReference.isActive()) {
                enqueueNotification(key, hash, valueReference, RemovalCause.COLLECTED);
                setValue(e, key, value, now);//存储数据,并且将新增加的元素写入两个队列中
                newCount = this.count; // count remains unchanged
              } else {
                setValue(e, key, value, now);
                newCount = this.count + 1;
              }
              this.count = newCount; // write-volatile
              evictEntries();//淘汰缓存
              return null;
            } else if (onlyIfAbsent) {
              // Mimic
              // "if (!map.containsKey(key)) ...
              // else return map.get(key);
              recordLockedRead(e, now);
              return entryValue;
            } else {
              // clobber existing entry, count remains unchanged
              // 如果存在且值不为null 则进行更新value
              ++modCount;
              enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED);
              setValue(e, key, value, now);
              evictEntries();
              return entryValue;
            }
          }
        }
    
        // Create a new entry. 不存在则新创建newEntry
        ++modCount;
        ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
        setValue(newEntry, key, value, now);
        table.set(index, newEntry);
        newCount = this.count + 1;
        this.count = newCount; // write-volatile
        evictEntries();
        return null;
      } finally {
        unlock();
        postWriteCleanup();
      }
    }
    

    LocalCache preWriteCleanup(now);

    简单解说下preWriteCleanup(now);preWriteCleanup在每次put之前都会清理动作

    • 通过下面代码的调用链:清理的是keyReferenceQueue和valueReferenceQueue这两个队列,这两个队列是引用队列,Guava Cache为了支持弱引用和软引用,引入了引用清空队列.

    • expireEntries(now) 基于过期时间的清除方式

    @GuardedBy("this")
    void preWriteCleanup(long now) {
      runLockedCleanup(now);
    }
    
    void runLockedCleanup(long now) {
      if (tryLock()) {
        try {
          drainReferenceQueues();
          expireEntries(now); // calls drainRecencyQueue
          readCount.set(0);
        } finally {
          unlock();
        }
      }
    }
    //排空键和值引用队列,清除包含垃圾收集的键或值的内部条目
    @GuardedBy("this")
    void drainReferenceQueues() {
      if (map.usesKeyReferences()) {
        drainKeyReferenceQueue();
      }
      if (map.usesValueReferences()) {
        drainValueReferenceQueue();
      }
    }
    //基于过期时间的清除
    @GuardedBy("this")
    void expireEntries(long now) {
      drainRecencyQueue();
    
      ReferenceEntry<K, V> e;
      while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
      while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
    }
    

    drainKeyReferenceQueue
    下面就是清理过程:

    @GuardedBy("this")
    void drainKeyReferenceQueue() {
      Reference<? extends K> ref;
      int i = 0;
      while ((ref = keyReferenceQueue.poll()) != null) {
        @SuppressWarnings("unchecked")
        ReferenceEntry<K, V> entry = (ReferenceEntry<K, V>) ref;
        map.reclaimKey(entry);
        if (++i == DRAIN_MAX) {
          break;
        }
      }
    }
    
    void reclaimKey(ReferenceEntry<K, V> entry) {
        int hash = entry.getHash();
        segmentFor(hash).reclaimKey(entry, hash);
    }
    
    /**
     * Removes an entry whose key has been garbage collected.
     */
    boolean reclaimKey(ReferenceEntry<K, V> entry, int hash) {
      lock();
      try {
        int newCount = count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);
    
        for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
          if (e == entry) {
            ++modCount;
            ReferenceEntry<K, V> newFirst = removeValueFromChain(
                first, e, e.getKey(), hash, e.getValueReference(), RemovalCause.COLLECTED);
            newCount = this.count - 1;
            table.set(index, newFirst);
            this.count = newCount; // write-volatile
            return true;
          }
        }
    
        return false;
      } finally {
        unlock();
        postWriteCleanup();
      }
    }
    

    LocalCache get获取缓存值

    Cache 接口声明

    @Beta
    @GwtCompatible
    public interface Cache<K, V> {
       V get(K key) throws ExecutionException;
    }
    

    LocalLoadingCache的实现

    下面是get方法的调用链:最终执行segmentFor(hash).get(key, hash, loader);

    • 首先通过key 与 散列值获取Entry,如果获取Entry不为null;继续获取对应的value;如果value不为null,并更新访问时间,加入recencyQueue,之后进行判断是否进行刷新逻辑 返回。

    • 如果value为null,检测是否进行loading,如果是则等待,等待结果waitForLoadingValue(e, key, valueReference);

    • 如果获取Entry为null,则lockedGetOrLoad(key, hash, loader); ,其方法逻辑实现与put方法非常相似,这里就不在做介绍。有兴趣的读者可以去看看。也需要说明的时候,此时该方法相当有就是写缓存,所以也会进行加锁。

    @Override
    public V get(K key) throws ExecutionException {
      return localCache.getOrLoad(key);
    }
    
    V getOrLoad(K key) throws ExecutionException {
      return get(key, defaultLoader); 
    }
    
    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
      int hash = hash(checkNotNull(key));
      return segmentFor(hash).get(key, hash, loader);
    }
    
    V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        if (count != 0) { // read-volatile volatile读会刷新缓存,尽量保证可见性,如果为0那么直接load
          // don't call getLiveEntry, which would ignore loading values
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {//判断通过key与hash获取的Entry是否为null,不为null则存在
            long now = map.ticker.read();//获取当前的访问时间
            V value = getLiveValue(e, now);//根据当前访问时间获取Live的数据
            if (value != null) {
              recordRead(e, now);//设置entry的AccessTime。并且加入recencyQueue
              statsCounter.recordHits(1);//记录缓存命中
              return scheduleRefresh(e, key, hash, value, now, loader)// 如果定时刷新,尝试刷新value
            }
            //value为null,如果此时value正在刷新,那么此时等待刷新结果
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }
    
        // at this point e is either null or expired;
        return lockedGetOrLoad(key, hash, loader);
      } catch (ExecutionException ee) {
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
          throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
          throw new UncheckedExecutionException(cause);
        }
        throw ee;
      } finally {
        postReadCleanup();//每次Put和get之后都要进行一次Clean
      }
    }
    

    至此针对 Guava Cache的使用,以及它的运转流程做了一个简单的介绍。可能水平有限如果有不正确的地方请指正。


    相关文章

      网友评论

          本文标题:02 初识缓存-GuavaCache

          本文链接:https://www.haomeiwen.com/subject/fjqvtctx.html