《Guava缓存工具》学习笔记

Guava通过接口LoadingCache提供了一个非常强大的基于内存的LoadingCache<K,V>。在缓存中自动加载值,它提供了许多实用的方法,在有缓存需求时非常有用。

 

接口声明

 

以下是forcom.google.common.cache.LoadingCache<K,V>接口的声明:

 

@Beta
@GwtCompatible
public interface LoadingCache<K,V>
   extends Cache<K,V>, Function<K,V>

 

 

接口方法

 

 

 

LoadingCache 示例

 

使用所选择的编辑器创建下面的java程序 C:/> Guava


GuavaTester.java

 

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.google.common.base.MoreObjects;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

public class GuavaTester {
   public static void main(String args[]){
      //create a cache for employees based on their employee id
      LoadingCache employeeCache = 
         CacheBuilder.newBuilder()
            .maximumSize(100) // maximum 100 records can be cached
            .expireAfterAccess(30, TimeUnit.MINUTES) // cache will expire after 30 minutes of access
            .build(new CacheLoader(){ // build the cacheloader
               @Override
               public Employee load(String empId) throws Exception {
                  //make the expensive call
                  return getFromDatabase(empId);
               }							
            });

      try {			
         //on first invocation, cache will be populated with corresponding
         //employee record
         System.out.println("Invocation #1");
         System.out.println(employeeCache.get("100"));
         System.out.println(employeeCache.get("103"));
         System.out.println(employeeCache.get("110"));
         //second invocation, data will be returned from cache
         System.out.println("Invocation #2");
         System.out.println(employeeCache.get("100"));
         System.out.println(employeeCache.get("103"));
         System.out.println(employeeCache.get("110"));

      } catch (ExecutionException e) {
         e.printStackTrace();
      }
   }

   private static Employee getFromDatabase(String empId){
      Employee e1 = new Employee("Mahesh", "Finance", "100");
      Employee e2 = new Employee("Rohan", "IT", "103");
      Employee e3 = new Employee("Sohan", "Admin", "110");

      Map database = new HashMap();
      database.put("100", e1);
      database.put("103", e2);
      database.put("110", e3);
      System.out.println("Database hit for" + empId);
      return database.get(empId);		
   }
}

class Employee {
   String name;
   String dept;
   String emplD;

   public Employee(String name, String dept, String empID){
      this.name = name;
      this.dept = dept;
      this.emplD = empID;
   }
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getDept() {
      return dept;
   }
   public void setDept(String dept) {
      this.dept = dept;
   }
   public String getEmplD() {
      return emplD;
   }
   public void setEmplD(String emplD) {
      this.emplD = emplD;
   }

   @Override
   public String toString() {
      return MoreObjects.toStringHelper(Employee.class)
      .add("Name", name)
      .add("Department", dept)
      .add("Emp Id", emplD).toString();
   }	
}原文出自【易百教程】,商业转载请联系作者获得授权,非商业请保留原文链接:https://www.yiibai.com/guava/guava_caching_utilities.html

 

 

验证结果

 

使用javac编译器如下编译类

 

C:\Guava>javac GuavaTester.java

 

现在运行GuavaTester看到的结果

 

C:\Guava>java GuavaTester

 

看看结果

 

Invocation #1
Database hit for100
Employee{Name=Mahesh, Department=Finance, Emp Id=100}
Database hit for103
Employee{Name=Rohan, Department=IT, Emp Id=103}
Database hit for110
Employee{Name=Sohan, Department=Admin, Emp Id=110}
Invocation #2
Employee{Name=Mahesh, Department=Finance, Emp Id=100}
Employee{Name=Rohan, Department=IT, Emp Id=103}
Employee{Name=Sohan, Department=Admin, Emp Id=110}

还是不太明白,继续看

  • guava cache的优点和使用场景,用来判断业务中是否适合使用此缓存
  • 介绍常用的方法,并给出示例,作为使用的参考
  • 深入解读源码。

guava简介

guava cache是一个本地缓存。有以下优点:

  • 很好的封装了get、put操作,能够集成数据源。
    一般我们在业务中操作缓存,都会操作缓存和数据源两部分。如:put数据时,先插入DB,再删除原来的缓存;ge数据时,先查缓存,命中则返回,没有命中时,需要查询DB,再把查询结果放入缓存中。 guava cache封装了这么多步骤,只需要调用一次get/put方法即可。
  • 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
  • Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。
  • 监控缓存加载/命中情况。

常用方法

  • V getIfPresent(Object key) 获取缓存中key对应的value,如果缓存没命中,返回null。return value if cached, otherwise return null.
  • V get(K key) throws ExecutionException 获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存。 return value if cached, otherwise load, cache and return.
  • void put(K key, V value) if cached, return; otherwise create, cache , and return.
  • void invalidate(Object key); 删除缓存
  • void invalidateAll(); 清楚所有的缓存,相当远map的clear操作。
  • long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。
  • CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。
  • ConcurrentMap<K, V> asMap(); 将缓存存储到一个线程安全的map中。

批量操作就是循环调用上面对应的方法,如:

  • ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
  • void putAll(Map<? extends K,? extends V> m);
  • void invalidateAll(Iterable<?> keys);
  • 复制代码

    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import java.util.concurrent.TimeUnit;
    
    public class guavaSample {
    
        public static void main(String[] args) {
    
            LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                    .maximumSize(10)  //最多存放十个数据
                    .expireAfterWrite(10, TimeUnit.SECONDS)  //缓存200秒
                    .recordStats()   //开启 记录状态数据功能
                    .build(new CacheLoader<String, Integer>() {
                        //数据加载,默认返回-1,也可以是查询操作,如从DB查询
                        @Override
                        public Integer load(String key) throws Exception {
                            return -1;
                        }
                    });
    
            //只查询缓存,没有命中,即返回null。 miss++
            System.out.println(cache.getIfPresent("key1")); //null
            //put数据,放在缓存中
            cache.put("key1", 1);
            //再次查询,已存在缓存中, hit++
            System.out.println(cache.getIfPresent("key1")); //1
            //失效缓存
            cache.invalidate("key1");
            //失效之后,查询,已不在缓存中, miss++
            System.out.println(cache.getIfPresent("key1")); //null
            
            try{
                //查询缓存,未命中,调用load方法,返回-1. miss++
                System.out.println(cache.get("key2"));   //-1
                //put数据,更新缓存
                cache.put("key2", 2);
                //查询得到最新的数据, hit++
                System.out.println(cache.get("key2"));    //2
                System.out.println("size :" + cache.size());  //1
    
                //插入十个数据
                for(int i=3; i<13; i++){
                    cache.put("key"+i, i);
                }
                //超过最大容量的,删除最早插入的数据,size正确
                System.out.println("size :" + cache.size());  //10
                //miss++
                System.out.println(cache.getIfPresent("key2"));  //null
    
                Thread.sleep(5000); //等待5秒
                cache.put("key1", 1);
                cache.put("key2", 2);
                //key5还没有失效,返回5。缓存中数据为key1,key2,key5-key12. hit++
                System.out.println(cache.getIfPresent("key5")); //5
    
                Thread.sleep(5000); //等待5秒
                //此时key5-key12已经失效,但是size没有更新
                System.out.println("size :" + cache.size());  //10
                //key1存在, hit++
                System.out.println(cache.getIfPresent("key1")); //1
                System.out.println("size :" + cache.size());  //10
                //获取key5,发现已经失效,然后刷新缓存,遍历数据,去掉失效的所有数据, miss++
                System.out.println(cache.getIfPresent("key5")); //null
                //此时只有key1,key2没有失效
                System.out.println("size :" + cache.size()); //2
    
                System.out.println("status, hitCount:" + cache.stats().hitCount()
                    + ", missCount:" + cache.stats().missCount()); //4,5
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
        }
    }

    复制代码

     

    guava cache源码解析

    先了解一些主要类和接口:

    • CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
      CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。这种初始化参数的方法值得借鉴,代码简洁易读。
    • CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作。
    • Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。
    • AbstractCache:抽象类,实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义。
    • LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。
    • AbstractLoadingCache:抽象类,继承自AbstractCache,实现LoadingCache接口。
    • LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。
    • LocalManualCache:LocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量localCache(LocalCache类型)的相应方法。
    • LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。

    综上,guava cache的核心操作,都在LocalCache中实现。
    其他:CacheStats:缓存加载/命中统计信息。

    在看具体的代码之前,先来简单了解一下LocalCache的数据结构。
    LocalCache的数据结构如下所示:

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

    CacheBuilder

    CacheBuilder是缓存配置和构建入口,先看一些属性。CacheBuilder的设置操作都是为这些属性赋值。

        //缓存的默认初始化大小

    复制代码

     //缓存的默认初始化大小
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
        // LocalCache默认并发数,用来评估Segment的个数
        private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
        //默认的缓存过期时间
        private static final int DEFAULT_EXPIRATION_NANOS = 0;
        
        static final int UNSET_INT = -1;
        int initialCapacity = UNSET_INT;//初始缓存大小
        int concurrencyLevel = UNSET_INT;//用于计算有几个并发
        long maximumSize = UNSET_INT;//cache中最多能存放的缓存entry个数
        long maximumWeight = UNSET_INT;
        
        Strength keyStrength;//键的引用类型(strong、weak、soft)
        Strength valueStrength;//值的引用类型(strong、weak、soft)
    
        long expireAfterWriteNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改)
        long expireAfterAccessNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改或被访问)
        //元素被移除的监听器
         RemovalListener<? super K, ? super V> removalListener;
         //状态计数器,默认为NULL_STATS_COUNTER,即不启动计数功能
         Supplier<? extends StatsCounter> statsCounterSupplie

    复制代码

     

    CacheBuilder构建缓存有两个方法:

    复制代码

    //构建一个具有数据加载功能的缓存
      public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
          CacheLoader<? super K1, V1> loader) {
        checkWeightWithWeigher();
        //调用LocalCache构造方法
        return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
      }
      
      //构建一个没有数据加载功能的缓存
       public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
        checkWeightWithWeigher();
        checkNonLoadingCache();
        //调用LocalCache构造方法,但loader为null
        return new LocalCache.LocalManualCache<K1, V1>(this);
      }
    
     //被CacheBuilder的build方法调用,将其参数传递给LocalCache
      LocalCache(
          CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
        //默认并发水平是4,即四个Segment(但要注意concurrencyLevel不一定等于Segment个数)
        //Segment个数:一个刚刚大于或等于concurrencyLevel且是2的几次方的一个数,在后面会有segmentCount赋值过程    
        concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
    
        keyStrength = builder.getKeyStrength();//默认为Strong,即强引用
        valueStrength = builder.getValueStrength();//默认为Strong,即强引用
    
        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);
        }
    
       //调整segmentCount个数,通过位移实现,所以是2的n次方。
        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);
        //每个segment的大小
        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);
          //初始化table
          initTable(newEntryArray(initialCapacity));
         //key引用队列
          keyReferenceQueue = map.usesKeyReferences()
               ? new ReferenceQueue<K>() : null;
         //value引用队列
          valueReferenceQueue = map.usesValueReferences()
               ? new ReferenceQueue<V>() : null;
    
          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

    LocalCache是guava cache的核心类。LocalCache的构造函数在上面已经分析过,接着看下核心方法。

    对于get(key, loader)方法流程:

    • 对key做hash,找到存储的segment及数组table上的位置;
    • 链表上查找entry,如果entry不为空,且value没有过期,则返回value,并刷新entry。
    • 若链表上找不到entry,或者value已经过期,则调用lockedGetOrLoad。
    • 锁住整个segment,遍历entry可能在的链表,查看数据是否存在是否过期,若存在则返回。若过期则删除(table,各种queue)。若不存在,则新建一个entry插入table。放开整个segment的锁。
    • 锁住entry,调用loader的reload方法,从数据源加载数据,然后调用storeLoadedValue更新缓存。
    • storeLoadedValue时,锁住整个segment,将value设置到entry中,并设置相关数据(入写入/访问队列,加载/命中数据等)。

    getAll(keys)方法:

    • 循环调用get方法,从缓存中获取key对应的value。没有命中的记录下来。
    • 如果有没有命中的key,调用loadAll(keys,loader)方法加载数据。
    • 将加载的数据依次缓存,调用segment的put(K key, int hash, V value, boolean onlyIfAbsent)方法。
    • put时,锁住整个segment,将数据插入链表,更新统计数据。

    put(key,value)方法:

    • 对key做hash,找到segment的位置和table上的位置;
    • 锁住整个segment,将数据插入链表,更新统计数据。

    putAll(map) 循环调用put方法。

    putIfAbsent(key, value) 缓存中,键值对不存在的时候才插入。

    实践

    guava cache是将数据源中的数据缓存在本地,那如果我们想把远端数据源中的数据缓存在远端 分布式缓存(如redis),可以怎么来使用guava cache的方式进行封装呢?
    可以仿照guava写一个简单的缓存,定义如下:
    CacheBuilder类 : 配置缓存参数,构建缓存。同上面所讲。
    Cache接口:定义增删查接口。
    MyCache类:实现Cache接口,put -> 存入DB,更新缓存; get -> 查询缓存,存在即返回;若不存在,查询DB,更新缓存,返回。
    CacheLoader类:供MyCache调用,get和getAll时提供单次查DB和批量查DB。

 转自:https://www.cnblogs.com/hejunnuo/p/10334888.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值