农场主的黑科技.

Guava的Cache

字数统计: 1.1k阅读时长: 6 min
2018/11/20 Share

在项目规模不大的时候可以用guava等提供的cache来代替redis.guava的cache可以设置removalListener来监听要移除缓存的数据。需要把过期或失效的缓存刷入数据库时很有用,用法如下:

设置一个大小为1,过期时间为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
public static Cache<Integer, Integer> cache =
CacheBuilder.newBuilder().maximumSize(1).expireAfterWrite(2, TimeUnit.SECONDS).removalListener(new RemovalListener<Integer, Integer>() {
@Override
public void onRemoval(RemovalNotification<Integer, Integer> removalNotification) {
System.out.println("getCause " + removalNotification.getCause());
System.out.println("wasEvicted " + removalNotification.wasEvicted());
System.out.println(removalNotification.getKey()+" " +removalNotification.getValue());
}
}).build();

public static void main(String[] args) throws InterruptedException {
System.out.println("===================== test1 ==================");
cache.put(1, 2);
cache.put(3, 4); //输出key=1 的 onRemoval,
cache.invalidate(3); //输出key=3 的 onRemoval

}
--------------------------------
输出
===================== test1 ==================
getCause SIZE
wasEvicted true
1 2
getCause EXPLICIT
wasEvicted false
3 4

另一个例子,可以看出guava不会超出timeout的元素及时调用onRemoval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cache.put(12, 34);
cache.put(56, 78); //输出key=12 的 onRemoval
System.out.println("key 12:"+ cache.getIfPresent(12));//返回null,超出size,会被新的替换
System.out.println("================= sleep 3 s");
TimeUnit.SECONDS.sleep(3);
System.out.println("key 56:"+ cache.getIfPresent(56)); //返回null,过期
cache.cleanUp(); //输出key=56 的 onRemoval,必须执行这个才会去检查是否过期, 否则尽管过期也不会触发onRemoval
-----------------
输出
===================== test2 ==================
getCause SIZE
wasEvicted true
12 34
key 12:null
================= sleep 3 s
key 56:null
getCause EXPIRED
wasEvicted true
56 78

关于这个问题文档中有解释

Caches built with CacheBuilder do not perform cleanup and evict values “automatically,” or instantly after a value expires, or anything of the sort. Instead, it performs small amounts of maintenance during write operations, or during occasional read operations if writes are rare.

The reason for this is as follows: if we wanted to perform Cache maintenance continuously, we would need to create a thread, and its operations would be competing with user operations for shared locks. Additionally, some environments restrict the creation of threads, which would make CacheBuilder unusable in that environment.

简单地说就是因为及时检查过期需要另外开启一个线程,会与用户线程发生锁的竞争,并且某些环境下会限制锁的创建.所以guava没有采用这种方式。

必须手动使用cache.cleanUp();来手动检查过期状态,此时过期的缓存才会触发removalListener。

刷新和过期

对于同一个key,用新的value替换旧value时也会触发removelisner。把上面代码的size设为.maximumSize(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cache.put(5, 6);
cache.put(7, 8);
TimeUnit.SECONDS.sleep(1);
cache.put(7, 9); //替换新值


TimeUnit.SECONDS.sleep(1);
System.out.println("key 5:" + cache.getIfPresent(5));
System.out.println("key 7:" + cache.getIfPresent(7));
-----------------
输出
getCause REPLACED
wasEvicted false
7 8
key 5:null
key 7:9

去get(7)的时候返回了新的值,而不是null,说明此时还未过期.当vlaue值被替换时,过期时间都会被调到最初

还有一个问题,上面说了当一个键发生更新时它的过期时间会被调到最初.那么当超出size时是先顶替掉最久没更新还时最早放入的. 写段代码来验证一下.同样是.maximumSize(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
cache.put(7, 8);
cache.put(10, 11); //最久没更新
cache.put(7, 9); //最早放入

cache.put(100, 100); //顶掉一个缓存
---------------------
输出
getCause REPLACED
wasEvicted false
7 8
getCause SIZE
wasEvicted true
10 11

可以看出,当超出size时实现顶替掉剩余过期时间最短的,也就是最久没更新的那个.

异步使用removalListener

我们可以通过removalListener来开启数据库连接并刷新数据库,但数据库操作往往很耗时。

可以通过异步的方式来执行onRemoval,做个测试,把大小设为1,连续放入4个值,会触发3次removalListener.在removalListener触发sleep

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
public static Cache<Integer, Integer> cache =
CacheBuilder.newBuilder()
.maximumSize(1)
.expireAfterWrite(2, TimeUnit.SECONDS)
.removalListener(RemovalListeners.asynchronous(new RemovalListener<Integer, Integer>() {
@Override
public void onRemoval(RemovalNotification<Integer, Integer> removalNotification) {
try {
TimeUnit.SECONDS.sleep(2); //耗时2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date());
}
},Executors.newWorkStealingPool())).build();

public static void main(String[] args) throws InterruptedException {
cache.put(12, 34);
cache.put(56, 78); //cacheSize = 1 ,只能包含一个
cache.put(89, 10);
cache.put(11, 12);

TimeUnit.SECONDS.sleep(5);
}
----------------------------------
输出
Tue Nov 20 13:23:06 JST 2018
Tue Nov 20 13:23:06 JST 2018
Tue Nov 20 13:23:06 JST 2018

可以看出虽然RemovalListener会耗时2秒,但由于异步并发执行,几乎同时返回。

相关资料:Guava CacheBuilder doesn’t call removal listener

CATALOG
  1. 1. 刷新和过期
  2. 2. 异步使用removalListener