缓存的重要性
简而言之,缓存的原理就是利用空间来换取时间。通过将数据存到访问速度更快的空间里以便下一次访问时直接从空间里获取,从而节省时间。
我们以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() {
- @Override
- public long read() {
- return 0;
- }
- })
- .build(new CacheLoader<String, Optional<T>>() {
- @Override
- public Optional<T> load(String key) {
- T cacheObject = getObjectFromDb(key);
- log.debug("find the local guava cache of key: {} is {}", key, cacheObject);
- return Optional.ofNullable(cacheObject);
- }
- });
- public T get(String key) {
- try {
- if (StrUtil.isEmpty(key)) {
- return null;
- }
- Optional<T> optional = guavaCache.get(key);
- return optional.orElse(null);
- } catch (ExecutionException e) {
- log.error("get cache object from guava cache failed.");
- e.printStackTrace();
- return null;
- }
- }
- public void invalidate(String key) {
- if (StrUtil.isEmpty(key)) {
- return;
- }
- guavaCache.invalidate(key);
- }
- public void invalidateAll() {
- guavaCache.invalidateAll();
- }
- /**
- * 从数据库加载数据
- * @param id
- * @return
- */
- public abstract T getObjectFromDb(Object id);
- }
复制代码 我们将getObjectFromDb方法留给子类自己去实现。以下是例子:- /**
- * @author valarchie
- */
- @Component
- @Slf4j
- @RequiredArgsConstructor
- public class GuavaCacheService {
- @NonNull
- private ISysDeptService deptService;
- public final AbstractGuavaCacheTemplate<SysDeptEntity> deptCache = new AbstractGuavaCacheTemplate<SysDeptEntity>() {
- @Override
- public SysDeptEntity getObjectFromDb(Object id) {
- return deptService.getById(id.toString());
- }
- };
- }
复制代码 Redis(三级缓存)
项目中会持续增长的数据比如用户、订单等相关数据。这些数据比较多,不适合放在内存级缓存当中,而应放在缓存中间件Redis当中去。Redis是支持持久化的,当我们的服务器重新启动时,依然可以从Redis中加载我们原先存储好的数据。
但是使用Redis缓存还有一个可以优化的点。我们可以自己本地再做一个局部的缓存来缓存Redis中的数据来减少网络IO请求,提高数据访问速度。 比如我们Redis缓存中有一万个用户的数据,但是一分钟之内可能只有不到1000个用户在请求数据。我们便可以在Redis中嵌入一个局部的Guava缓存来提供性能。以下是RedisCacheTemplate.
[code]/** * 缓存接口实现类 三级缓存 * @author valarchie */@Slf4jpublic class RedisCacheTemplate { private final RedisUtil redisUtil; private final CacheKeyEnum redisRedisEnum; private final LoadingCache guavaCache; public RedisCacheTemplate(RedisUtil redisUtil, CacheKeyEnum redisRedisEnum) { this.redisUtil = redisUtil; this.redisRedisEnum = redisRedisEnum; this.guavaCache = CacheBuilder.newBuilder() // 基于容量回收。缓存的最大数量。超过就取MAXIMUM_CAPACITY = 1 |