google开源库-guava缓存

guava是什么?

Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。

guava的优势?

  • 标准化 - Guava库是由谷歌托管。
  • 高效 - 可靠,快速和有效的扩展JAVA标准库
  • 优化 -Guava库经过高度的优化

本文章主要讲解guava的缓存策略

缓存需要考虑的几大因素:

  • 缓存更新规则(主动、获取时更新、回源)
  • 获取缓存-gol策略(同步、异步)
  • 缓存清理规则(主动、定时_局部、数量、容量)

缓存辅助功能

  • 命中率
  • 缓存移除回调通知

缓存GOL策略

同步
//@Param timeout:过期时间
CacheBuilder.newBuilder()
	//某时间段内,读/写未访问调用load方法重新加载数据
	.expireAfterAccess(long time,TimeUnit time)
	.build(new CacheLoader<String, Object>() {
       @Override
       public Object load(String key) throws Exception {
           String value = "";//这里根据key,实现具体的获得value逻辑
           return value;
       }
   });

同步策略A:当key值第一次被访问或time范围内没有读写操作,调用load方法获取value值
优化点:当某个过期key被并发访问,那么就会有很多线程穿透guava获取数据,此类场景很容易造成雪崩效应。guava的处理办法是只允许一个线程获取数据,其他线程进行阻塞,直接获取新的数据

CacheBuilder.newBuilder()
   .refreshAfterWrite(long data,TimeUnit time)
   //某时间段内,更新未访问调用reload方法进行reflush
   .expireAfterAccess(long time,TimeUnit time)
   .build(new CacheLoader<String, Object>() {
      @Override
      public Object load(String key) throws Exception {
          String value = "";//这里根据key,实现具体的获得value逻辑
          return value;
      };
  });

同步策略B:当某个未过期key被并发访问且上次reflush超过了指定时间,抢到锁的某个线程调用reload方法重新获取数据
优化点:解决了同步策略A其他线程阻塞的问题,但抢到锁资源的线程同样会阻塞

异步
CacheBuilder.newBuilder()
.refreshAfterWrite(long data,TimeUnit time)
//某时间段内,更新未访问调用reload方法进行reflush
.expireAfterAccess(long time,TimeUnit time)
.build(new CacheLoader<String, Object>() {
     @Override
     public Object load(String key) throws Exception {
         String value = “”;//这里根据key,实现具体的获得value逻辑
         return value;
     };
     @Override
      public ListenableFuture<Object> reload(final String key,  Object oldValue) throws Exception {
          return executorService.submit(new Callable<Object>() {
          @Override
          public Object call() throws Exception {
             {
                     //dosomething
                     return value;
             } });                                                                     
        }
 });  

异步策略:当某个未过期key被并发访问且上次reflush超过了指定时间,抢到锁的某个线程异步调用reload方法重新获取数据
优化点:解决了同步策略B抢到锁资源的线程会阻塞的问题,但完全异步需要构建一个线程池executorService,线程池的具体参数需视场景而定

注:使用reflush策略会产生一个问题,当某个key很长一段时间未访问,重新获得key值至少会有一个或多个线程返回的是旧值,这是因为guava的key值过期更新策略大部分是在gol的时候触发,如果追求数据的一致性,可以调用CacheBuilder.newBuilder().build().invalidate(key)方法强制刷新。


缓存清理规则

·设定全局数量

CacheBuilder.maximumSize(int num);

常用算法有FIFO(先进先出),LRU(最近访问优先),guava采用的是LRU算法
注:时间复杂度为O(1)的LRU算法,一般采用双链表+hash散列实现

·设定全局容量
CacheBuilder.maximumWeight(1024 * 1024 * 1024);

  CacheBuilder.newBuilder()
	.maximumWeight(1024 * 1024 * 1024) // 设置最大容量为 1M
	.weigher(new Weigher() { 
		@Override
		public int weigh(String key, String value) {
			return key.getBytes().length + value.getBytes().length;
		}
	}).build();

. guava的容量大小可由客户端自己实现

·键值的定期清理

CacheBuilder.newBuilder()
	.expireAfterAccess(long time,TimeUnit unit) ;

CacheBuilder.newBuilder()
	.expireAfterWrite(long time,TimeUnit unit) ;
		
	. 规定时间范围内没有更新/查询操作,清理键值使用expireAfterAccess
	. 规定时间范围内没有更新操作,清理键值使用expireAfterWrite

·主动清除

//清理单个key
CacheBuilder.newBuilder().build().invalidate(key);
//清理全部key
CacheBuilder.newBuilder().build().invalidateAll();


guava gol分析

google开源库-guava缓存_第1张图片


guava entry寻址

第一步:
hash后高位截取 & (捅size-1),寻到具体的Segument对象

Segment<K, V> segmentFor(int hash) {
  // TODO(fry): Lazily create segments?
  return segments[(hash >>> segmentShift) & segmentMask];
}

第二步:
hash & (AtomicReferenceArray的size-1),寻到具体的ReferenceEntry链表

ReferenceEntry<K, V> getFirst(int hash) {
  // read this volatile field only once
  AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
  return table.get(hash & (table.length() - 1));
}

第三步:
遍历链表获取entry对象,同时尝试调用tryDrainReferenceQueues()清理弱引用的数据

ReferenceEntry<K, V> getEntry(Object key, int hash) {
  for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
    if (e.getHash() != hash) {
      continue;}
    K entryKey = e.getKey();
    if (entryKey == null) {
      tryDrainReferenceQueues();
      continue; }
    if (map.keyEquivalence.equivalent(key, entryKey)) {
      return e;
    }
  } return null; }

图解
google开源库-guava缓存_第2张图片


guava entry过期数据清理

通过队列poll遍历recencyQueue,先将entry插入到accessQueue头部(LRU算法的体现)。

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();
    }
  }
}
//清理之前的动作
 void drainRecencyQueue() {
    ReferenceEntry<K, V> e;
    while ((e = recencyQueue.poll()) != null) {
      // An entry may be in the recency queue despite it being removed from
      // the map . This can occur when the entry was concurrently read while a
      // writer is removing it from the segment or after a clear has removed
      // all of the segment's entries.
      if (accessQueue.contains(e)) {
        accessQueue.add(e);
      }
    }
  }
entry超过阈值的清理流程

假设插入数据时,超过了每个Segment捅设置的最大值,从accessQueue队列尾部循环清理,直到小于阈值。
注:maxSegmentWeight=maxWeight/segmentSize,所以在设置阈值的时候,尽量要比真实值大一些。

 void evictEntries() {
   if (!map.evictsBySize()) {
     return;
   }
   drainRecencyQueue();
   //超过了每个Segment捅的最大值
   while (totalWeight > maxSegmentWeight) {
     ReferenceEntry<K, V> e = getNextEvictable();
     if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
       throw new AssertionError();
     }
   }
 }
 //清理顺序
 ReferenceEntry<K, V> getNextEvictable() {
   for (ReferenceEntry<K, V> e : accessQueue) {
     int weight = e.getValueReference().getWeight();
     if (weight > 0) {
       return e;  }  }
   throw new AssertionError(); }


guava LRU算法
  Segment<K, V> createSegment(
      int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) {
    return new Segment<K, V>(this, initialCapacity, maxSegmentWeight, statsCounter);
  }

初始化LocalCache时,会根据maxWeight构建一个Segments数组,Segments类似CurrentHashMap中捅的概念。当未设置maxWeight时,捅的大小是4,但捅在初始化后大小是不会变化的,所以maxNum或maxWeight最好配置一下。
我们来看看Segments的几个参数:

 Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight,
     StatsCounter statsCounter) {
   this.map = map;
   this.maxSegmentWeight = maxSegmentWeight;
   this.statsCounter = checkNotNull(statsCounter);
   initTable(newEntryArray(initialCapacity));

   keyReferenceQueue = map.usesKeyReferences()
        ? new ReferenceQueue<K>() : null;

   valueReferenceQueue = map.usesValueReferences()
        ? new ReferenceQueue<V>() : null;

   recencyQueue = map.usesAccessQueue()
       ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
       : LocalCache.<K, V>>discardingQueue();

   writeQueue = map.usesWriteQueue()
       ? new WriteQueue<K, V>()
       : LocalCache.<K, V>>discardingQueue();

   accessQueue = map.usesAccessQueue()
       ? new AccessQueue<K, V>()
       : LocalCache.<K, V>>discardingQueue();

 void initTable(AtomicReferenceArray<ReferenceEntry<K, V>> newTable) {
   this.threshold = newTable.length() * 3 / 4; // 0.75
   if (!map.customWeigher() && this.threshold == maxSegmentWeight) {
     // prevent spurious expansion before eviction
     this.threshold++;
   }
   this.table = newTable;
 }

AtomicReferenceArray> newTable
此数组用来维护缓存的kv数据,ReferenceEntry是一个单链表,AtomicReferenceArray大小由maxWeight和argument数量共同决定,当AtomicReferenceArray达到 newTable.length() * 3 / 4大小后,按2的倍数进行扩容。
accessQueue
此队列(含双链表功能)用来维护移除读写过期的数据,同时维护移除numMax或weightMax等超量的数据
writeQueue
此队列(含双链表功能)用来维护移除写过期的数据
recencyQueue
此队列就是个简单的单链表,查询时插入到recencyQueue中,包含了重复、null值数据,LRU的雏形
keyReferenceQueue,valueReferenceQueue
被动触发非强引用的数据k,v数据,并将AtomicReferenceArray中的队列元素移除


accessQueue讲解:
accessQueue继承自AbstractQueue,同时具备了双链表的功能。
accessQueue重写了队列add中的offer方法,主要逻辑是找到链表中的位置并剔除同时维护前后索引关系并将新元素加入到尾部

    static final class AccessQueue<K, V> extends AbstractQueue<ReferenceEntry<K, V>> {
    final ReferenceEntry<K, V> head = new AbstractReferenceEntry<K, V>()}@Override
    public boolean offer(ReferenceEntry<K, V> entry) {
      // unlink
      connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());

      // add to tail
      connectAccessOrder(head.getPreviousInAccessQueue(), entry);
      connectAccessOrder(entry, head);

      return true;
    }

recencyQueue讲解:
recencyQueue继承自ConcurrentLinkedQueue,属于线程安全的链表队列,recencyQueue只在guava调用查询、更新方法时触发add,entry的更新操作(删除、更新)并不会触发recencyQueue的poll方法,故recencyQueue包含了失效及重复的数据。
recencyQueue的主要目的通过队列记录每次访问的key,真正有数据需要expire、evict时触发调整accessQueue链表元素的位置。 accessQueue才是真正的LRU队列

@GuardedBy("this")
void drainRecencyQueue() {
  ReferenceEntry<K, V> e;
  while ((e = recencyQueue.poll()) != null) {
    // An entry may be in the recency queue despite it being removed from
    // the map . This can occur when the entry was concurrently read while a
    // writer is removing it from the segment or after a clear has removed
    // all of the segment's entries.
    if (accessQueue.contains(e)) {
      accessQueue.add(e);
    }
  }
}

注:update方法的LRU策略会在调用drainRecencyQueue后维护,这样就保证了LRU读写的顺序

void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {
  // we are already under lock, so drain the recency queue immediately
  drainRecencyQueue();
  totalWeight += weight;

  if (map.recordsAccess()) {
    entry.setAccessTime(now);
  }
  if (map.recordsWrite()) {
    entry.setWriteTime(now);
  }
  accessQueue.add(entry);
  writeQueue.add(entry);
}

guava reload分析

google开源库-guava缓存_第3张图片
loadAsyn()分为两步,1、调用loadFuture() reload数据 2、reload完成后,调用addListener()回调,回调内容是将数据通过线程池异步刷新到值栈中, 性能的提升仅限于未重写reload方法的用户


ListenableFuture<V> loadAsync(final K key, final int hash,
       final LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) {
     final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
     loadingFuture.addListener(
         new Runnable() {
           @Override
           public void run() {
             try {
               V newValue = getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
             } catch (Throwable t) {
               logger.log(Level.WARNING, "Exception thrown during refresh", t);
               loadingValueReference.setException(t);
             }
           }
         }, directExecutor());
     return loadingFuture;
   }

google开源库-guava缓存_第4张图片
注意:如未重写reload方法,将阻塞调用load方法加载数据

    @GwtIncompatible("Futures")
    public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(oldValue);
        return Futures.immediateFuture(this.load(key));
    }

你可能感兴趣的:(java,java,缓存,guava)