zoukankan      html  css  js  c++  java
  • guava缓存底层实现

    摘要

    guava的缓存相信很多人都有用到,

    Cache<String, String> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(100, TimeUnit.SECONDS)
            .maximumSize(10).build();

    也常用的方法是设置过期时间。但使用过程中会遇到一些问题:当过期时间到了,缓存中的对象真的会立即被释放吗?当缓存达到容量以后,如何高效的剔除缓存?guava cache的底层数据结构是如何的?带着这些问题,一起来看看guava cache的源码

    介绍一下guava缓存的基本框架

    image

    • LoacalCache:实现了currentMap接口,保存了一些配置信息,例如失效时间、容量等。是保存所有缓存最外层的容器
    • segment:为了高并发,借鉴了currentMap中的分段锁机制,segment可以理解是LocalCache中的一部分,不同的segment之间并发不受影响。每次操作根据key进行hash,保证了同一个key的put和set都在同一个segment中。segment中还有两个分别队列用于保存软引用或者弱引用对象回收后的引用
    • refrenceEntry:保存一个缓存key-val的对象,类似map中的entry,只不过map中entry保存的对象的直接进行,而refrenceEntry这是在中间多了一层valueReference
    • valueReference:如果是强引用,则直接保存对象的直接引用,当然也可以使用软引用的方法。

    其实通过和CurrentHashMap最类比比较好理解,只不过guava缓存在其基础上增强了缓存过期的机制:

    1. 最大对象个数限制
    2. 超时机制
    3. 弱引用或者软引用

    guava会oom吗

    答案是肯定的,当我们设置缓存用不过期(或者很长),缓存的对象不限个数(或者很大),例如

    Cache<String, String> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(100000, TimeUnit.SECONDS)
            .build();

    不断向guava加入缓存大字符串,最终将能oom,解决这种办法:

    使用弱引用或者软应用

    Cache<String, String> cache = CacheBuilder.newBuilder()
                    .expireAfterWrite(1, TimeUnit.SECONDS)
                    .weakValues().build();

    guava在创建对象放在一个map(LocalCache.class)的时候,默认使用强引用(StrongValueReference.class),如果指定使用弱引用的时候,就会创建的是(WeakValueReference.class)

    合适最大容量

    这个也是比较推荐的方法,根据业务需求,设置合适的缓存容量、这样超过容量以后,缓存就会按照LRU的方式回收缓存。

    CacheBuilder.maximumSize(10)

    guava缓存到期就会立即清除吗

    guava清楚过期缓存的机制是什么,是单独使用线程来扫描吗?不是的,是在每次进行缓存操作的时候,如get()或者put()的时候,判断缓存是否过期。核心代码

    void expireEntries(long now) {
      drainRecencyQueue(); //多线并发的情况下,防止误删access
    
      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();
        }
      }
    }

    其中 writeQueue是保存按照写入缓存先后时间的队列,每次get或者put都可能触发触发这个方法。accessQueue同理,对应的是最后访问失效时间的功能。
    因此可以看出,一个如果一个对象放入缓存以后,不在有任何缓存操作(包括对缓存其他key的操作),那么该缓存不会主动过期的。不过这种情况是极端情况下才会出现。

    guava如何找出最久未使用的缓存

    在上面也说到了,是用accessQueue,这个队列的实现比较复杂。这个队列其实是按照最久未使用的顺序存放的缓存对象(ReferenceEntry)的。由于会经常进行元素的移动,例如把访问过的对象放到队列的最后。ReferenceEntry这个在前面框架图里面说到了,使用来保存key-val的,其中接口包含一些特殊方法:

    @Override
    public ReferenceEntry<K, V> getNextInAccessQueue() {
      throw new UnsupportedOperationException();
    }
    
    @Override
    public void setNextInAccessQueue(ReferenceEntry<K, V> next) {
      throw new UnsupportedOperationException();
    }
    
    @Override
    public ReferenceEntry<K, V> getPreviousInAccessQueue() {
      throw new UnsupportedOperationException();
    }
    
    @Override
    public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) {
      throw new UnsupportedOperationException();
    }

    这样通过ReferenceEntry可以就可以判断该entry的后面节点,如果不在队列中,则返回一个NullEntry的对象。这样做的好处就弥补了 链表的缺点

    • 判断一个ReferenceEntry是否在队列中,只要判断该ReferenceEntry的前一个引用是否是NullEntry,不需要便利整个链表

    并且可以很方便的更新和删除链表中的节点,因为每次访问的时候都可能需要更新该链表,放入到链表的尾部,这样,每次从access中拿出的头节点就是最久未使用的。 并且,如果按照访问时间来删除缓存的时候,只要从队列里找出第一个访问没有超时的对象,那么之前遍历的缓存都是应该删除的,这样就不需要遍历整个缓存的对象来判断。

     

    对应的writeQueue用来保存最久未更新的缓存队列,实现方式和accessQueue一样。

    总结

    可以看出,guava缓存的原型是CurrentHashMap,在其基础上考虑如果判断缓存是否过期。底层的一些数据结构也是用的十分巧妙。如果能仔细的看看源码,相信对你也有一定的帮助

     

  • 相关阅读:
    一些比较水的题目
    oracle not in,not exists,minus 数据量大的时候的性能问题
    简单的oracle分页语句
    oracle 查询结果集运算
    Spring注解详解
    HTTP报头Accept 和 Content-Type的区别
    vue 实现分转元的 过滤器
    oracle or语句的坑
    CSS样式 让你的输入的小写自动变成大写。
    js 十分精确身份证验证
  • 原文地址:https://www.cnblogs.com/lizo/p/7235838.html
Copyright © 2011-2022 走看看