简介
缓存的种类有很多,需要根据不同的应用场景来选择不同的缓存方案,一种是分布式缓存如redis、memcached,另外一种是本地(进程内)缓存如:ehcache、GuavaCache、Caffeine。本文记录Guava Cache的一些使用。
Guava Cache 是Google的开源工具包 Guava 中提供的一个本地缓存工具,它支持高并发,并且是线程安全的,借鉴了ConcurrentHashMap的数据结构(一个支持LRU的ConcurrentHashMap,并提供了基于容量,时间和引用的缓存回收方式),在多线程情况下可以安全的访问或者更新缓存。
应用场景
本地缓存的数据读写都在一个进程内,相比 redis 等分布式缓存,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。
基于以上特点,guava cache 的主要应用场景为以下几种:
- 对于访问速度有较大要求
- 存储的数据不经常变化
- 数据量不大,占用内存较小
- 能够容忍数据不是实时的
使用
GuavaCache使用时主要分二种模式:LoadingCache、CallableCache。
核心区别在于: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 41
| @Service public class EmployService {
@AutoWired private EmployeeDao employeeDao
private final LoadingCache<String, Optional<Employee>> empLocalCache = CacheBuilder.newBuilder() .initialCapacity(512) .maximumSize(2048) .concurrencyLevel(5) .expireAfterWrite(1, TimeUnit.HOURS) .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 34
| public class TaskService {
private final static Cache<String, String> ERROR_CACHE = CacheBuilder.newBuilder() .initialCapacity(2048) .concurrencyLevel(5) .expireAfterWrite(4, TimeUnit.HOURS) .removalListener(notification -> System.out.println("notification=" + notification)) .build();
public void taskHandle { try {
} 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));
} } }
}
|
显式清除缓存
Guava Cache 提供了几种显式清除缓存条目的方法,允许你手动移除缓存中的某个或某些条目。
- 移除单个条目
使用 invalidate(key) 方法可以移除缓存中的特定键对应的条目。
- 移除多个条目
使用 invalidateAll(keys) 方法可以移除缓存中所有在给定集合中的键对应的条目。
1
| cache.invalidateAll(keys);
|
- 移除所有条目
使用 invalidateAll() 方法可以移除缓存中的所有条目。
- 注册移除监听器
可以在构建缓存时注册一个移除监听器(RemovalListener),它会在每次条目被移除时调用。
1 2 3 4 5 6 7 8 9 10 11
| Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder() .removalListener(new RemovalListener<KeyType, ValueType>() { @Override public void onRemoval(RemovalNotification<KeyType, ValueType> notification) { } }) .build();
|