Guava 缓存工具

简介

缓存的种类有很多,需要根据不同的应用场景来选择不同的缓存方案,一种是分布式缓存如redis、memcached,另外一种是本地(进程内)缓存如:ehcache、GuavaCache、Caffeine。本文记录Guava Cache的一些使用。

Guava Cache 是Google的开源工具包 Guava 中提供的一个本地缓存工具,它支持高并发,并且是线程安全的,借鉴了ConcurrentHashMap的数据结构(一个支持LRU的ConcurrentHashMap,并提供了基于容量,时间和引用的缓存回收方式),在多线程情况下可以安全的访问或者更新缓存。

应用场景

本地缓存的数据读写都在一个进程内,相比 redis 等分布式缓存,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。

基于以上特点,guava cache 的主要应用场景为以下几种:

  • 对于访问速度有较大要求
  • 存储的数据不经常变化
  • 数据量不大,占用内存较小
  • 能够容忍数据不是实时的

使用

GuavaCache使用时主要分二种模式:LoadingCacheCallableCache
核心区别在于:LoadingCache创建时需要有合理的默认方法来加载或计算与键关联的值(可以理解为当从缓存中get某个key对应的value时,如果value不存在,则调用load方法将结果放到缓存并返回),CallableCache创建时则没有这个限制,使用上更加简便灵活。

前置准备

引入jar包:

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>

LoadingCache使用

LoadingCache的典型使用场景:查询某个数据时,先从本地缓存中获取,如果本地缓存没有,再从数据库查,并将数据库查询的结果放入本地缓存。

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
@Service
public class EmployService {

@AutoWired
private EmployeeDao employeeDao


private final LoadingCache<String, Optional<Employee>> empLocalCache = CacheBuilder.newBuilder()
.initialCapacity(512) // 内部哈希表的初始容量
.maximumSize(2048) // 最大缓存数量
.concurrencyLevel(5) // 并发等级,也可以定义为同时操作缓存的线程数。根据官方文档描述默认值为4,但后续版本可能会有调整。
.expireAfterWrite(1, TimeUnit.HOURS) // 每条记录的缓存过期时间。不同key根据放入缓存的时间单独算过期时间
.build(new CacheLoader<String, Optional<Employee>>() {
@Override
public Optional<Employee> load(@NotNull String empCode) throws Exception {
// 自动写缓存数据的方法,必须要实现
Employee employee = employeeDao.selectEmployee(empCode);
log.debug("员工信息加载至本地缓存|empCode={}|{}", empCode, JSONUtil.toJSON(employee));
return Optional.ofNullable(employee);
}
});


// 根据工号从查询员工信息,使用本地缓存
public Employee getEmployeeByCode(String empCode) {
if (StringUtils.isEmpty(empCode)) {
return null;
}
try {
Optional<Employee> optional = empLocalCache.get(empCode);
if (optional.isPresent()) {
return optional.get();
}
} catch (ExecutionException e) {
log.error("根据工号查询员工信息异常|empCode={}", empCode,e);
}
return null;
}

}

上面这段代码中,本地缓存的value没有直接使用 Employee 对象,而是使用 Optional 对象进行了封装,原因是 LoadingCache 是不支持缓存null值的,如果load()方法返回null,则在get(key)的时候会抛出异常:CacheLoader returned null for key。但实际情况中 null 的情况有可能是存在的,推荐做法是使用 Optional 对象来封装结果,这样缓存的 value 就永远不可能为空,而真正的业务数据是否为空则可通过 Optional.ifPresent()来判断。

CallableCache使用

CallableCache的一个使用场景举例:某个定时任务每分钟会进行一次任务扫描,如果任务不为空则进行任务处理,如果任务执行异常则发送短信或企业微信提醒。因为扫描间隔比较短,为了防止一条异常任务执行失败而导致短信提醒得太过频繁,可以用一个本地缓存来控制。如下是示例:

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


private final static Cache<String, String> ERROR_CACHE = CacheBuilder.newBuilder()
.initialCapacity(2048)
.concurrencyLevel(5)
//设置cache中的数据在写入之后的过期时间
.expireAfterWrite(4, TimeUnit.HOURS)
//当Entry被移除时的监听器
.removalListener(notification -> System.out.println("notification=" + notification))
.build();


public void taskHandle {
try {

// todo 任务处理......

} catch (Exception e) {
log.error("任务处理异常,id:{}", id, e);
String errMsg = e.getMessage();
String cacheKey = "TASK_" + id + "_" + errMsg;
String present = ERROR_CACHE.getIfPresent(cacheKey);
if (present == null) {
//为了防止一直发送提醒,在本地做一个缓存,一段时间内只会发送一次消息提醒
ERROR_CACHE.put(cacheKey, DateTime.now().toString(DatePattern.PURE_DATETIME_PATTERN));

// todo 发送消息提醒......
}
}
}

}

显式清除缓存

Guava Cache 提供了几种显式清除缓存条目的方法,允许你手动移除缓存中的某个或某些条目。

  1. 移除单个条目
    使用 invalidate(key) 方法可以移除缓存中的特定键对应的条目。

    1
    cache.invalidate(key);
  2. 移除多个条目
    使用 invalidateAll(keys) 方法可以移除缓存中所有在给定集合中的键对应的条目。

    1
    cache.invalidateAll(keys);
  3. 移除所有条目
    使用 invalidateAll() 方法可以移除缓存中的所有条目。

    1
    cache.invalidateAll();
  4. 注册移除监听器
    可以在构建缓存时注册一个移除监听器(RemovalListener),它会在每次条目被移除时调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9

    Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
    .removalListener(new RemovalListener<KeyType, ValueType>() {
    @Override
    public void onRemoval(RemovalNotification<KeyType, ValueType> notification) {
    // 处理移除事件
    }
    })
    .build();
------ 本文完 ------