Google Guava 在实际场景中的应用封装

原文:https://juejin.cn/post/6844903624519188487

毕竟西湖六月中,风光不与四时同。

接天莲叶无穷碧,映日荷花别样红。

晓出净慈寺送林子方-杨万里

周末与小伙伴约了一波西湖,这个时间荷花开的正好…,在开始文章之前先放一张“佛系”美图来镇楼!!!

最近这段时间用了下谷歌的 guava,自己封了一个缓存模板方案,特此记录,以备后续所需。

一个缓存定时清除任务带来的GC问题

为什么要从这个来说起,因为不说这个就没 guava 什么事了!

最近项目中需要使用缓存来对一查查询频繁的数据做缓存处理;首先我们也不希望引入三方的如redis或者memcache这样的服务进来,其次是我们对于数据一致性的要求并不是很高,不需要集群内的查询接口共享到一份缓存数据;所以这样一来我们只要实现一个基于内存的缓存即可。

最开始我并没有考虑使用guava来做这个事情,而是自己写了一套基于CurrentHashMap的缓存方案;这里需要明确一点,因为缓存在这个场景里面希望提供超时清除的能力,而基于所以在自己缓存框架中增加了定时清除过期数据的能力。

这里我就直接把定时清楚的这段代码放上来:

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
 /**
* 静态内部类来进行超时处理
*/
private class ClearCacheThread extends Thread {
@Override
public void run() {
while (true){
try {
long now = System.currentTimeMillis();
Object[] keys = map.keySet().toArray();
for (Object key : keys) {
CacheEntry entry = map.get(key);
if (now - entry.time >= cacheTimeout) {
synchronized (map) {
map.remove(key);
if (LOGGER.isDebugEnabled()){
LOGGER.debug("language cache timeout clear");
}
}
}
}
}catch (Exception e){
LOGGER.error("clear out time cache value error;",e);
}
}
}
}

这个线程是用来单独处理过期数据的。缓存初始化时就会触发这个线程的start方法开始执行。

正式由于这段代码的不合理导致我在发布dev环境之后,机器GC触发的频次高的离谱。在尝试了不同的修复方案之后,最后选择放弃了;改用guava了!

小伙伴们可以在下面留言来讨论下这里为什么会存在频繁GC的问题;我会把结论放在评论回复里面。

guava

为什么选用guava呢,很显然,是大佬推荐的!!!

guava是谷歌提供的一个基于内存的缓存工具包,Guava Cache 提供了一种把数据(key-value对)缓存到本地(JVM)内存中的机制,适用于很少会改动的数据。Guava Cache 与 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache 为了限制内存占用,通常都设定为自动回收元素。

对于我们的场景,guava 提供的能力满足了我们的需要:

  • 数据改动小
  • 基于内存
  • 可以自动回收

既然选择它了,我们还是有必要来先对它有个大致的了解;先来看看它提供的一些类和接口:

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

整个来看的话,guava里面最核心的应该算是 LocalCache 这个类了。

1
2
3
@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements
ConcurrentMap<K, V>

关于这个类的源码这里就不细说了,直接来看下在实际应用中我的封装思路【封装满足我当前的需求,如果有小伙伴需要借鉴,可以自己在做扩展】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final int            MAX_SIZE     = 1000;
private static final int EXPIRE_TIME = 10;
private static final int DEFAULT_SIZE = 100;

private int maxSize = MAX_SIZE;
private int expireTime = EXPIRE_TIME;
/** 时间单位(分钟) */
private TimeUnit timeUnit = TimeUnit.MINUTES;
/** Cache初始化或被重置的时间 */
private Date resetTime;

/** 分别记录历史最多缓存个数及时间点*/
private long highestSize = 0;
private Date highestTime;

private volatile LoadingCache<K, V> cache;

这里先是定义了一些常量和基本的属性信息,当然这些属性会提供set&get方法,供实际使用时去自行设置。

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
public LoadingCache<K, V> getCache() {
//使用双重校验锁保证只有一个cache实例
if(cache == null){
synchronized (this) {
if(cache == null){
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
cache = CacheBuilder.newBuilder()
//设置缓存容器的初始容量为100
.initialCapacity(DEFAULT_SIZE)
//缓存数据的最大条目
.maximumSize(maxSize)
//定时回收:缓存项在给定时间内没有被写访问(创建或覆盖),则回收。
.expireAfterWrite(expireTime, timeUnit)
//启用统计->统计缓存的命中率等
.recordStats()
//设置缓存的移除通知
.removalListener((notification)-> {
if (LOGGER.isDebugEnabled()){
LOGGER.debug("{} was removed, cause is {}" ,notification.getKey(), notification.getCause());
}
})
.build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
return fetchData(key);
}
});
this.resetTime = new Date();
this.highestTime = new Date();
if (LOGGER.isInfoEnabled()){
LOGGER.info("本地缓存{}初始化成功.", this.getClass().getSimpleName());
}
}
}
}

return cache;
}

上面这段代码是整个缓存的核心,通过这段代码来生成我们的缓存对象【使用了单例模式】。具体的属性参数看注释。

因为上面的那些都是封装在一个抽象类AbstractGuavaCache里面的,所以我又封装了一个CacheManger用来管理缓存,并对外提供具体的功能接口;在CacheManger中,我使用了一个静态内部类来创建当前默认的缓存。

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
/**
* 使用静态内部类实现一个默认的缓存,委托给manager来管理
*
* DefaultGuavaCache 使用一个简单的单例模式
* @param <String>
* @param <Object>
*/
private static class DefaultGuavaCache<String, Object> extends
AbstractGuavaCache<String, Object> {

private static AbstractGuavaCache cache = new DefaultGuavaCache();

/**
* 处理自动载入缓存,按实际情况载入
* 这里
* @param key
* @return
*/
@Override
protected Object fetchData(String key) {
return null;
}

public static AbstractGuavaCache getInstance() {
return DefaultGuavaCache.cache;
}

}

大概思路就是这样,如果需要扩展,我们只需要按照实际的需求去扩展AbstractGuavaCache这个抽象类就可以了。具体的代码贴在下面了。

完整的两个类

AbstractGuavaCache

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
public abstract class AbstractGuavaCache<K, V> {

protected final Logger LOGGER = LoggerFactory.getLogger(AbstractGuavaCache.class);

private static final int MAX_SIZE = 1000;
private static final int EXPIRE_TIME = 10;
/** 用于初始化cache的参数及其缺省值 */
private static final int DEFAULT_SIZE = 100;

private int maxSize = MAX_SIZE;

private int expireTime = EXPIRE_TIME;
/** 时间单位(分钟) */
private TimeUnit timeUnit = TimeUnit.MINUTES;
/** Cache初始化或被重置的时间 */
private Date resetTime;

/** 分别记录历史最多缓存个数及时间点*/
private long highestSize = 0;
private Date highestTime;

private volatile LoadingCache<K, V> cache;

public LoadingCache<K, V> getCache() {
//使用双重校验锁保证只有一个cache实例
if(cache == null){
synchronized (this) {
if(cache == null){
//CacheBuilder的构造函数是私有的,只能通过其静态方法ne
//wBuilder()来获得CacheBuilder的实例
cache = CacheBuilder.newBuilder()
//设置缓存容器的初始容量为100
.initialCapacity(DEFAULT_SIZE)
//缓存数据的最大条目
.maximumSize(maxSize)
//定时回收:缓存项在给定时间内没有被写访问
//(创建或覆盖),则回收。
.expireAfterWrite(expireTime, timeUnit)
//启用统计->统计缓存的命中率等
.recordStats()
//设置缓存的移除通知
.removalListener((notification)-> {
if (LOGGER.isDebugEnabled()){
//...
}
})
.build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
return fetchData(key);
}
});
this.resetTime = new Date();
this.highestTime = new Date();
if (LOGGER.isInfoEnabled()){
//...
}
}
}
}

return cache;
}

/**
* 根据key从数据库或其他数据源中获取一个value,并被自动保存到缓存中。
*
* 改方法是模板方法,子类需要实现
*
* @param key
* @return value,连同key一起被加载到缓存中的。
*/
protected abstract V fetchData(K key);

/**
* 从缓存中获取数据(第一次自动调用fetchData从外部获取数据),并处理异常
* @param key
* @return Value
* @throws ExecutionException
*/
protected V getValue(K key) throws ExecutionException {
V result = getCache().get(key);
if (getCache().size() > highestSize) {
highestSize = getCache().size();
highestTime = new Date();
}
return result;
}

public int getMaxSize() {
return maxSize;
}

public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}

public int getExpireTime() {
return expireTime;
}

public void setExpireTime(int expireTime) {
this.expireTime = expireTime;
}

public TimeUnit getTimeUnit() {
return timeUnit;
}

public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}

public Date getResetTime() {
return resetTime;
}

public void setResetTime(Date resetTime) {
this.resetTime = resetTime;
}

public long getHighestSize() {
return highestSize;
}

public void setHighestSize(long highestSize) {
this.highestSize = highestSize;
}

public Date getHighestTime() {
return highestTime;
}

public void setHighestTime(Date highestTime) {
this.highestTime = highestTime;
}
}

DefaultGuavaCacheManager

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
public class DefaultGuavaCacheManager {

private static final Logger LOGGER =
LoggerFactory.getLogger(DefaultGuavaCacheManager.class);
//缓存包装类
private static AbstractGuavaCache<String, Object> cacheWrapper;

/**
* 初始化缓存容器
*/
public static boolean initGuavaCache() {
try {
cacheWrapper = DefaultGuavaCache.getInstance();
if (cacheWrapper != null) {
return true;
}
} catch (Exception e) {
LOGGER.error("Failed to init Guava cache;", e);
}
return false;
}

public static void put(String key, Object value) {
cacheWrapper.getCache().put(key, value);
}

/**
* 指定缓存时效
* @param key
*/
public static void invalidate(String key) {
cacheWrapper.getCache().invalidate(key);
}

/**
* 批量清除
* @param keys
*/
public static void invalidateAll(Iterable<?> keys) {
cacheWrapper.getCache().invalidateAll(keys);
}

/**
* 清除所有缓存项 : 慎用
*/
public static void invalidateAll() {
cacheWrapper.getCache().invalidateAll();
}

public static Object get(String key) {
try {
return cacheWrapper.getCache().get(key);
} catch (Exception e) {
LOGGER.error("Failed to get value from guava cache;", e);
}
return null;
}

/**
* 使用静态内部类实现一个默认的缓存,委托给manager来管理
*
* DefaultGuavaCache 使用一个简单的单例模式
* @param <String>
* @param <Object>
*/
private static class DefaultGuavaCache<String, Object> extends
AbstractGuavaCache<String, Object> {

private static AbstractGuavaCache cache = new DefaultGuavaCache();

/**
* 处理自动载入缓存,按实际情况载入
* @param key
* @return
*/
@Override
protected Object fetchData(String key) {
return null;
}

public static AbstractGuavaCache getInstance() {
return DefaultGuavaCache.cache;
}

}

}

参考

Google Guava官方教程(中文版)

作者

卫恒

发布于

2018-06-25

更新于

2022-05-25

许可协议

评论