LoadingCache

 

缓存,在我们日常开发中是必不可少的一种解决性能问题的方法。简单的说,cache 就是为了提升系统性能而开辟的一块内存空间。

 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用。在日常开发的很多场合,由于受限于硬盘IO的?

 缓存在很多系统和架构中都用广泛的应用,例如:

 1.CPU缓存 
 2.操作系统缓存 
 3.本地缓存 
 4.分布式缓存 
 5.HTTP缓存 
 6.数据库缓存 
 等等,可以说在计算机和网络领域,缓存无处不在。可以这么说,只要有硬件性能不对等,涉及到网络传输的地方都会有缓存的身影。


1.生成一个LoadingCache对象

  1.  
  2. LoadingCache userCache = CacheBuilder.newBuilder()

  3. .maximumSize(10000))//设置缓存上线

  4. .expireAfterAccess(10, TimeUnit.MINUTES)//设置时间对象没有被读/写访问则对象从内存中删除

  5. .expireAfterWrite(10, TimeUnit.MINUTES)//设置时间对象没有被写访问则对象从内存中删除

  6. //移除监听器,缓存项被移除时会触发

  7. .removalListener(new RemovalListener() {

  8. @Override

  9. public void onRemoval(RemovalNotification notification) {

  10. //逻辑

  11. }

  12. }

  13. })

  14. .recordStats()

  15. //CacheLoader类 实现自动加载

  16. .build(new CacheLoader() {

  17. @Override

  18. public Object load(String key) {

  19. //从SQL或者NoSql 获取对象

  20. }

  21. });

  22.  

2.CacheBuilder方法 
1) LoadingCache build(CacheLoader loader) : LoadingCache对象创建

  1. public LoadingCache build(

  2. CacheLoader loader) {

  3. checkWeightWithWeigher();

  4. return new LocalCache.LocalLoadingCache(this, loader);

  5. }

2)CacheBuilder.maximumSize(long size)方法:配置缓存数量上限,快达到上限或达到上限,处理了时间最长没被访问过的对象或者根据配置的被释放的对象

3)expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样

4)expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

5)refreshAfterWrite(long duration, TimeUnit unit): 定时刷新,可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新,即只有刷新间隔时间到了你再去get(key)才会重新去执行Loading否则就算刷新间隔时间到了也不会执行loading操作。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。还有一点比较重要的是refreshAfterWrite和expireAfterWrite两个方法设置以后,重新get会引起loading操作都是同步串行的。这其实可能会有一个隐患,当某一个时间点刚好有大量检索过来而且都有刷新或者回收的话,是会产生大量的请求同步调用loading方法,这些请求占用线程资源的时间明显变长。如正常请求也就20ms,当刷新以后加上同步请求loading这个功能接口可能响应时间远远大于20ms。为了预防这种井喷现象,可以不设refreshAfterWrite方法,改用LoadingCache.refresh(K)因为它是异步执行的,不会影响正在读的请求,同时使用ScheduledExecutorService可以帮助你很好地实现这样的定时调度,配上cache.asMap().keySet()返回当前所有已加载键,这样所有的key定时刷新就有了。如果访问量没有这么大则直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。这个可以评估自己的项目实际情况来决策。


统计相关: 
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

hitRate():缓存命中率;

averageLoadPenalty():加载新值的平均时间,单位为纳秒;

evictionCount():缓存项被回收的总数,不包括显式清除

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

asMap视图

asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;

asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。

所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。


3.LoadingCache方法的使用 
1)V get(K k): 内部调用getOrLoad(K key)方法,缓存中有对应的值则返回,没有则使用CacheLoader load方法 
getOrLoad(K key)方法为线程安全方法,内部加锁 
2)V getIfPresent(Object key):缓存中有对应的值则返回,没有则返回NULL

  1. @Nullable

  2. public V getIfPresent(Object key) {

  3. int hash = hash(checkNotNull(key));

  4. V value = segmentFor(hash).get(key, hash);

  5. if (value == null) {

  6. globalStatsCounter.recordMisses(1);

  7. } else {

  8. globalStatsCounter.recordHits(1);

  9. }

  10. return value;

  11. }

3)ImmutableMap getAll(Iterable keys) :提供一组keys筛选出符合条件的所有值。内部调用遍历keys调用get(K key)方法获得已经缓存的对象,没有缓存的对象则通过调用CacheLoader.loadAll方法加载,如果没实现loadAll方法则会抛出UnsupportedLoadingOperationException异常,处理这个异常最终会遍历每个key通过lockedGetOrLoad(key, hash, loader)方法调用CacheLoader.load方法,实现加载成功

  1. ImmutableMap getAll(Iterable keys) throws ExecutionException {

  2. int hits = 0;

  3. int misses = 0;

  4.  
  5. Map result = Maps.newLinkedHashMap();

  6. Set keysToLoad = Sets.newLinkedHashSet();

  7. for (K key : keys) {

  8. V value = get(key);

  9. if (!result.containsKey(key)) {

  10. result.put(key, value);

  11. if (value == null) {

  12. misses++;

  13. keysToLoad.add(key);

  14. } else {

  15. hits++;

  16. }

  17. }

  18. }

  19.  
  20. try {

  21. if (!keysToLoad.isEmpty()) {

  22. try {

  23. Map newEntries = loadAll(keysToLoad, defaultLoader);

  24. for (K key : keysToLoad) {

  25. V value = newEntries.get(key);

  26. if (value == null) {

  27. throw new InvalidCacheLoadException("loadAll failed to return a value for " + key);

  28. }

  29. result.put(key, value);

  30. }

  31. } catch (UnsupportedLoadingOperationException e) {

  32. // loadAll not implemented, fallback to load

  33. for (K key : keysToLoad) {

  34. misses--; // get will count this miss

  35. result.put(key, get(key, defaultLoader));

  36. }

  37. }

  38. }

  39. return ImmutableMap.copyOf(result);

  40. } finally {

  41. globalStatsCounter.recordHits(hits);

  42. globalStatsCounter.recordMisses(misses);

  43. }

  44. }

4) ImmutableMap getAll(Iterable keys): 提供一组keys筛选出符合条件缓存中存在的所有值

  1. ImmutableMap getAllPresent(Iterable keys) {

  2. int hits = 0;

  3. int misses = 0;

  4.  
  5. Map result = Maps.newLinkedHashMap();

  6. for (Object key : keys) {

  7. V value = get(key);

  8. if (value == null) {

  9. misses++;

  10. } else {

  11. // TODO(fry): store entry key instead of query key

  12. @SuppressWarnings("unchecked")

  13. K castKey = (K) key;

  14. result.put(castKey, value);

  15. hits++;

  16. }

  17. }

  18. globalStatsCounter.recordHits(hits);

  19. globalStatsCounter.recordMisses(misses);

  20. return ImmutableMap.copyOf(result);

  21. }

5) long size() : 缓存对象数量

6)put(K key,V value): 直接显示地向缓存中插入值,这会直接覆盖掉已有键之前映射的值。

7)invalidate(Object key):显式地清除指定key的缓存对象

 
  1. public void invalidate(Object key) {

  2. checkNotNull(key);

  3. localCache.remove(key);

  4. }

8) invalidateAll(Iterable keys) : 清除批量缓存对象

 
  1. public void invalidateAll(Iterable keys) {

  2. localCache.invalidateAll(keys);

  3. }

  4. void invalidateAll(Iterable keys) {
  5.  
  6. // TODO(fry): batch by segment

  7. for (Object key : keys) {

  8. remove(key);

  9. }

  10. }

9)invalidateAll(): 清除所有缓存对象

  1. public void invalidateAll() {

  2. localCache.clear();

  3. }

10) public void refresh(K key) :刷新指定key的缓存对象,刷新和回收不太一样。刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。重载CacheLoader.reload可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值

11)ConcurrentMap asMap():获取缓存数据转换成Map类型


关于guava Cache数据移除:

  guava做cache时候数据的移除方式,在guava中数据的移除分为被动移除和主动移除两种。 
  被动移除数据的方式,guava默认提供了三种方式: 
  1.基于大小的移除:看字面意思就知道就是按照缓存的大小来移除,如果即将到达指定的大小,那就会把不常用的键值对从cache中移除。 
  定义的方式一般为 CacheBuilder.maximumSize(long),还有一种一种可以算权重的方法,个人认为实际使用中不太用到。就这个常用的来看有几个注意点, 
    其一,这个size指的是cache中的条目数,不是内存大小或是其他; 
    其二,并不是完全到了指定的size系统才开始移除不常用的数据的,而是接近这个size的时候系统就会开始做移除的动作; 
    其三,如果一个键值对已经从缓存中被移除了,你再次请求访问的时候,如果cachebuild是使用cacheloader方式的,那依然还是会从cacheloader中再取一次值,如果这样还没有,就会抛出异常 
  2.基于时间的移除:guava提供了两个基于时间移除的方法 
    expireAfterAccess(long, TimeUnit) 这个方法是根据某个键值对最后一次访问之后多少时间后移除 
    expireAfterWrite(long, TimeUnit) 这个方法是根据某个键值对被创建或值被替换后多少时间移除 
  3.基于引用的移除: 
  这种移除方式主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除 
  主动移除数据方式,主动移除有三种方法: 
  1.单独移除用 Cache.invalidate(key) 
  2.批量移除用 Cache.invalidateAll(keys) 
  3.移除所有用 Cache.invalidateAll() 
  如果需要在移除数据的时候有所动作还可以定义Removal Listener,但是有点需要注意的是默认Removal Listener中的行为是和移除动作同步执行的,如果需要改成异步形式,可以考虑使用RemovalListeners.asynchronous(RemovalListener, Executor)

你可能感兴趣的:(LoadingCache,LoadingCache)