缓存的重要性
简而言之,缓存的原理就是利用空间来换取时间。通过将数据存到访问速度更快的空间里以便下一次访问时直接从空间里获取,从而节省时间。
我们以CPU的缓存体系为例:
CPU缓存体系是多层级的。分成了CPU -> L1 -> L2 -> L3 -> 主存。我们可以得到以下启示。
- 越频繁使用的数据,使用的缓存速度越快
- 越快的缓存,它的空间越小
而我们项目的缓存设计可以借鉴CPU多级缓存的设计。
关于多级缓存体系实现在开源项目中:https://github.com/valarchie/AgileBoot-Back-End
缓存分层
首先我们可以给缓存进行分层。在Java中主流使用的三类缓存主要有:
- Map(原生缓存)
- Guava/Caffeine(功能更强大的内存缓存)
- Redis/Memcached(缓存中间件)
在一些项目中,会一刀切将所有的缓存都使用Redis或者Memcached中间件进行存取。
使用缓存中间件避免不了网络请求成本和用户态和内核态的切换。 更合理的方式应该是根据数据的特点来决定使用哪个层级的缓存。
Map(一级缓存)
项目中的字典类型的数据比如:性别、类型、状态等一些不变的数据。我们完全可以存在Map当中。
因为Map的实现非常简单,效率上是非常高的。由于我们存的数据都是一些不变的数据,一次性存好并不会再去修改它们。所以不用担心内存溢出的问题。 以下是关于字典数据使用Map缓存的简单代码实现。
/**
* 本地一级缓存 使用Map
*
* @author valarchie
*/
public class MapCache {
private static final Map<String, List<DictionaryData>> DICTIONARY_CACHE = MapUtil.newHashMap(128);
static {
initDictionaryCache();
}
private static void initDictionaryCache() {
loadInCache(BusinessTypeEnum.values());
loadInCache(YesOrNoEnum.values());
loadInCache(StatusEnum.values());
loadInCache(GenderEnum.values());
loadInCache(NoticeStatusEnum.values());
loadInCache(NoticeTypeEnum.values());
loadInCache(OperationStatusEnum.values());
loadInCache(VisibleStatusEnum.values());
}
public static Map<String, List<DictionaryData>> dictionaryCache() {
return DICTIONARY_CACHE;
}
private static void loadInCache(DictionaryEnum[] dictionaryEnums) {
DICTIONARY_CACHE.put(getDictionaryName(dictionaryEnums[0].getClass()), arrayToList(dictionaryEnums));
}
private static String getDictionaryName(Class<?> clazz) {
Objects.requireNonNull(clazz);
Dictionary annotation = clazz.getAnnotation(Dictionary.class);
Objects.requireNonNull(annotation);
return annotation.name();
}
@SuppressWarnings("rawtypes")
private static List<DictionaryData> arrayToList(DictionaryEnum[] dictionaryEnums) {
if(ArrayUtil.isEmpty(dictionaryEnums)) {
return ListUtil.empty();
}
return Arrays.stream(dictionaryEnums).map(DictionaryData::new).collect(Collectors.toList());
}
}
Guava(二级缓存)
项目中的一些自定义数据比如角色,部门。这种类型的数据往往不会非常多。而且请求非常频繁。比如接口中经常要校验角色相关的权限。我们可以使用Guava或者Caffeine这种内存框架作为二级缓存使用。
Guava或者Caffeine的好处可以支持缓存的过期时间以及缓存的淘汰,避免内存溢出。
以下是利用模板设计模式做的GuavaCache模板类。
/**
* 缓存接口实现类 二级缓存
* @author valarchie
*/
@Slf4j
public abstract class AbstractGuavaCacheTemplate<T> {
private final LoadingCache<String, Optional<T>> guavaCache = CacheBuilder.newBuilder()
// 基于容量回收。缓存的最大数量。超过就取MAXIMUM_CAPACITY = 1 << 30。依靠LRU队列recencyQueue来进行容量淘汰
.maximumSize(1024)
// 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用。设置最大总权重
// 没写访问下,超过5秒会失效(非自动失效,需有任意put get方法才会扫描过期失效数据。但区别是会开一个异步线程进行刷新,刷新过程中访问返回旧数据)
.refreshAfterWrite(5L, TimeUnit.MINUTES)
// 移除监听事件
.removalListener(removal -> {
// 可做一些删除后动作,比如上报删除数据用于统计
log.info("触发删除动作,删除的key={}, value={}", removal.getKey(), removal.getValue());
})
// 并行等级。决定segment数量的参数,concurrencyLevel与maxWeight共同决定
.concurrencyLevel(16)
// 开启缓存统计。比如命中次数、未命中次数等
.recordStats()
// 所有segment的初始总容量大小
.initialCapacity(128)
// 用于测试,可任意改变当前时间。参考:https://www.geek-share.com/detail/2689756248.html
.ticker(new Ticker() {
@Over