Guava LoadingCache详解及工具类

一、Guava介绍

Guava是Google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中。实际项目开发中经常将一些公共或者常用的数据缓存起来方便快速访问。

Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果不符合需求,可以选择Memcached、Redis等工具。

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

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

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

  1. CPU缓存
  2. 操作系统缓存
  3. 本地缓存
  4. 分布式缓存
  5. HTTP缓存
  6. 数据库缓存

 等等,可以说在计算机和网络领域,缓存无处不在。可以这么说,只要有硬件性能不对等,涉及到网络传输的地方都会有缓存的身影。

二、代码示例

1. POM引入

1
2
3
4
5
6
7
8
9
<dependency>

  <groupId>com.google.guava</groupId>

  <artifactId>guava</artifactId>

  <version>28.1-jre</version>

</dependency>

2. 封装工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package com.soyoung.ad.engine.util;
 
import com.google.common.cache.*;
import lombok.extern.slf4j.Slf4j;
 
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
/**
 * 功能描述
 *
 */
@Slf4j
public class CacheManager {
 
  /** 缓存项最大数量 */
  private static final long GUAVA_CACHE_SIZE = 100000;
 
  /** 缓存时间:天 */
  private static final long GUAVA_CACHE_DAY = 10;
 
  /** 缓存操作对象 */
  private static LoadingCache<Long, String> GLOBAL_CACHE = null;
 
  static {
    try {
      GLOBAL_CACHE = loadCache(new CacheLoader<Long, String>() {
        @Override
        public String load(Long key) throws Exception {
          // 处理缓存键不存在缓存值时的处理逻辑
          return "";
        }
      });
    } catch (Exception e) {
      log.error("初始化Guava Cache出错", e);
    }
  }
 
  /**
   * 全局缓存设置
   *
   * 缓存项最大数量:100000
   * 缓存有效时间(天):10
   *
   *
   * @param cacheLoader
   * @return
   * @throws Exception
   */
  private static LoadingCache<Long, String> loadCache(CacheLoader<Long, String> cacheLoader) throws Exception {
    LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
        .maximumSize(GUAVA_CACHE_SIZE)
        //设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
        .expireAfterAccess(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        // 设置缓存在写入之后 设定时间 后失效
        .expireAfterWrite(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        //移除监听器,缓存项被移除时会触发
        .removalListener(new RemovalListener<Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //逻辑操作
          }
        })
        //开启Guava Cache的统计功能
        .recordStats()
        .build(cacheLoader);
    return cache;
  }
 
  /**
   * 设置缓存值
   * 注: 若已有该key值,则会先移除(会触发removalListener移除监听器),再添加
   *
   * @param key
   * @param value
   */
  public static void put(Long key, String value) {
    try {
      GLOBAL_CACHE.put(key, value);
    } catch (Exception e) {
      log.error("设置缓存值出错", e);
    }
  }
 
  /**
   * 批量设置缓存值
   *
   * @param map
   */
  public static void putAll(Map<? extends Long, ? extends String> map) {
    try {
      GLOBAL_CACHE.putAll(map);
    } catch (Exception e) {
      log.error("批量设置缓存值出错", e);
    }
  }
 
  /**
   * 获取缓存值
   * 注:如果键不存在值,将调用CacheLoader的load方法加载新值到该键中
   *
   * @param key
   * @return
   */
  public static String get(Long key) {
    String token = "";
    try {
      token = GLOBAL_CACHE.get(key);
    } catch (Exception e) {
      log.error("获取缓存值出错", e);
    }
    return token;
  }
 
  /**
   * 移除缓存
   *
   * @param key
   */
  public static void remove(Long key) {
    try {
      GLOBAL_CACHE.invalidate(key);
    } catch (Exception e) {
      log.error("移除缓存出错", e);
    }
  }
 
  /**
   * 批量移除缓存
   *
   * @param keys
   */
  public static void removeAll(Iterable<Long> keys) {
    try {
      GLOBAL_CACHE.invalidateAll(keys);
    } catch (Exception e) {
      log.error("批量移除缓存出错", e);
    }
  }
 
  /**
   * 清空所有缓存
   */
  public static void removeAll() {
    try {
      GLOBAL_CACHE.invalidateAll();
    } catch (Exception e) {
      log.error("清空所有缓存出错", e);
    }
  }
 
  /**
   * 获取缓存项数量
   *
   * @return
   */
  public static long size() {
    long size = 0;
    try {
      size = GLOBAL_CACHE.size();
    } catch (Exception e) {
      log.error("获取缓存项数量出错", e);
    }
    return size;
  }
}

3.方法说明

1.生成一个LoadingCache对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 LoadingCache userCache = CacheBuilder.newBuilder()
                .maximumSize(10000))//设置缓存上线
                .expireAfterAccess(10, TimeUnit.MINUTES)//设置时间对象没有被读/写访问则对象从内存中删除
                .expireAfterWrite(10, TimeUnit.MINUTES)//设置时间对象没有被写访问则对象从内存中删除
                //移除监听器,缓存项被移除时会触发
                .removalListener(new RemovalListener<String, UserProfile>() {
                    @Override
                    public void onRemoval(RemovalNotification<String, UserProfile> notification) {
                       //逻辑
                        }
                    }
                })
                .recordStats()
                //CacheLoader类 实现自动加载
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) {
                       //从SQL或者NoSql 获取对象
                    }
                });

2.CacheBuilder方法
1) LoadingCache build(CacheLoader loader) :

LoadingCache对象创建

1
2
3
4
5
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);
  }

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
2
3
4
5
6
7
8
9
10
11
@Nullable
  public V getIfPresent(Object key) {
    int hash = hash(checkNotNull(key));
    V value = segmentFor(hash).get(key, hash);
    if (value == null) {
      globalStatsCounter.recordMisses(1);
    } else {
      globalStatsCounter.recordHits(1);
    }
    return value;
  }

3)ImmutableMap getAll(Iterable keys) :

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
    int hits = 0;
    int misses = 0;

    Map<K, V> result = Maps.newLinkedHashMap();
    Set<K> keysToLoad = Sets.newLinkedHashSet();
    for (K key : keys) {
      V value = get(key);
      if (!result.containsKey(key)) {
        result.put(key, value);
        if (value == null) {
          misses++;
          keysToLoad.add(key);
        } else {
          hits++;
        }
      }
    }

    try {
      if (!keysToLoad.isEmpty()) {
        try {
          Map<K, V> newEntries = loadAll(keysToLoad, defaultLoader);
          for (K key : keysToLoad) {
            V value = newEntries.get(key);
            if (value == null) {
              throw new InvalidCacheLoadException("loadAll failed to return a value for " + key);
            }
            result.put(key, value);
          }
        } catch (UnsupportedLoadingOperationException e) {
          // loadAll not implemented, fallback to load
          for (K key : keysToLoad) {
            misses--; // get will count this miss
            result.put(key, get(key, defaultLoader));
          }
        }
      }
      return ImmutableMap.copyOf(result);
    } finally {
      globalStatsCounter.recordHits(hits);
      globalStatsCounter.recordMisses(misses);
    }
  }

4) ImmutableMap getAll(Iterable keys):

提供一组keys筛选出符合条件缓存中存在的所有值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ImmutableMap<K, V> getAllPresent(Iterable<?> keys) {
    int hits = 0;
    int misses = 0;

    Map<K, V> result = Maps.newLinkedHashMap();
    for (Object key : keys) {
      V value = get(key);
      if (value == null) {
        misses++;
      } else {
        // TODO(fry): store entry key instead of query key
        @SuppressWarnings("unchecked")
        K castKey = (K) key;
        result.put(castKey, value);
        hits++;
      }
    }
    globalStatsCounter.recordHits(hits);
    globalStatsCounter.recordMisses(misses);
    return ImmutableMap.copyOf(result);
  }

5) long size() :

缓存对象数量

6)put(K key,V value):

直接显示地向缓存中插入值,这会直接覆盖掉已有键之前映射的值。

7)invalidate(Object key):

显式地清除指定key的缓存对象

1
2
3
4
public void invalidate(Object key) {
      checkNotNull(key);
      localCache.remove(key);
    }

8) invalidateAll(Iterable keys) :

清除批量缓存对象

1
2
3
4
5
6
7
8
9
10
public void invalidateAll(Iterable<?> keys) {
      localCache.invalidateAll(keys);
   }

void invalidateAll(Iterable<?> keys) {
    // TODO(fry): batch by segment
    for (Object key : keys) {
      remove(key);
    }
  }

9)invalidateAll():

清除所有缓存对象

1
2
3
public void invalidateAll() {
      localCache.clear();
    }

10) public void refresh(K key) :

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

11)ConcurrentMap asMap():

获取缓存数据转换成Map类型

三、使用总结

1. 移除机制

guava做cache时候数据的移除分为被动移除主动移除两种。

被动移除分为三种:

基于大小的移除:数量达到指定大小,会把不常用的键值移除

基于时间的移除:expireAfterAccess(long, TimeUnit) 根据某个键值对最后一次访问之后多少时间后移除
        expireAfterWrite(long, TimeUnit) 根据某个键值对被创建或值被替换后多少时间移除

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

主动移除分为三种:1).单独移除:Cache.invalidate(key)

         2).批量移除:Cache.invalidateAll(keys)

         3).移除所有:Cache.invalidateAll()

如果配置了移除监听器RemovalListener,则在所有移除的动作时会同步执行该listener下的逻辑。

如需改成异步,使用:RemovalListeners.asynchronous(RemovalListener, Executor)

2. 遇到的问题

在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加
配置了expireAfterAccess和expireAfterWrite,但在指定时间后没有被移除。

解决方案:CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为a)线程相对较重,b)某些环境限制线程的创建。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。

相关原文链接:

https://www.jb51.net/article/178569.htm

https://blog.csdn.net/zj380475045/article/details/76163323