缓存的重要性

简而言之,缓存的原理便是运用空间来交换时间。通过将数据存到拜访速度更快的空间里以便下一次拜访时直接从空间里获取,然后节省时间。
咱们以CPU的缓存系统为例:

项目中多级缓存设计实践总结

CPU缓存系统是多层级的。分成了CPU -> L1 -> L2 -> L3 -> 主存。咱们能够得到以下启示。

  • 越频频运用的数据,运用的缓存速度越快
  • 越快的缓存,它的空间越小

而咱们项目的缓存规划能够借鉴CPU多级缓存的规划。

关于多级缓存系统完成在开源项目中:github.com/valarchie/A…

缓存分层

首先咱们能够给缓存进行分层。在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.

/**
 * 缓存接口完成类 三级缓存
 * @author valarchie
 */
@Slf4j
public class RedisCacheTemplate<T> {
    private final RedisUtil redisUtil;
    private final CacheKeyEnum redisRedisEnum;
    private final LoadingCache<String, Optional<T>> guavaCache;
    public RedisCacheTemplate(RedisUtil redisUtil, CacheKeyEnum redisRedisEnum) {
        this.redisUtil = redisUtil;
        this.redisRedisEnum = redisRedisEnum;
        this.guavaCache = CacheBuilder.newBuilder()
            // 根据容量收回。缓存的最大数量。超越就取MAXIMUM_CAPACITY = 1 << 30。依靠LRU行列recencyQueue来进行容量筛选
            .maximumSize(1024)
            .softValues()
            // 没写拜访下,超越5秒会失效(非自动失效,需有恣意put get办法才会扫描过期失效数据。
            // 但区别是会开一个异步线程进行改写,改写过程中拜访回来旧数据)
            .expireAfterWrite(redisRedisEnum.expiration(), TimeUnit.MINUTES)
            // 并行等级。决议segment数量的参数,concurrencyLevel与maxWeight一起决议
            .concurrencyLevel(64)
            // 一切segment的初始总容量巨细
            .initialCapacity(128)
            .build(new CacheLoader<String, Optional<T>>() {
                @Override
                public Optional<T> load(String cachedKey) {
                    T cacheObject = redisUtil.getCacheObject(cachedKey);
                    log.debug("find the redis cache of key: {} is {}", cachedKey, cacheObject);
                    return Optional.ofNullable(cacheObject);
                }
            });
    }
    /**
     * 从缓存中获取目标   如果获取不到的话  从DB层面获取
     * @param id
     * @return
     */
    public T getObjectById(Object id) {
        String cachedKey = generateKey(id);
        try {
            Optional<T> optional = guavaCache.get(cachedKey);
//            log.debug("find the guava cache of key: {}", cachedKey);
            if (!optional.isPresent()) {
                T objectFromDb = getObjectFromDb(id);
                set(id, objectFromDb);
                return objectFromDb;
            }
            return optional.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 从缓存中获取 目标, 即便找不到的话 也不从DB中找
     * @param id
     * @return
     */
    public T getObjectOnlyInCacheById(Object id) {
        String cachedKey = generateKey(id);
        try {
            Optional<T> optional = guavaCache.get(cachedKey);
            log.debug("find the guava cache of key: {}", cachedKey);
            return optional.orElse(null);
        } catch (ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 从缓存中获取 目标, 即便找不到的话 也不从DB中找
     * @param cachedKey 直接通过redis的key来查找
     * @return
     */
    public T getObjectOnlyInCacheByKey(String cachedKey) {
        try {
            Optional<T> optional = guavaCache.get(cachedKey);
            log.debug("find the guava cache of key: {}", cachedKey);
            return optional.orElse(null);
        } catch (ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }
    public void set(Object id, T obj) {
        redisUtil.setCacheObject(generateKey(id), obj, redisRedisEnum.expiration(), redisRedisEnum.timeUnit());
        guavaCache.refresh(generateKey(id));
    }
    public void delete(Object id) {
        redisUtil.deleteObject(generateKey(id));
        guavaCache.refresh(generateKey(id));
    }
    public void refresh(Object id) {
        redisUtil.expire(generateKey(id), redisRedisEnum.expiration(), redisRedisEnum.timeUnit());
        guavaCache.refresh(generateKey(id));
    }
    public String generateKey(Object id) {
        return redisRedisEnum.key() + id;
    }
    public T getObjectFromDb(Object id) {
        return null;
    }
}

以下是运用办法:

/**
 * @author valarchie
 */
@Component
@RequiredArgsConstructor
public class RedisCacheService {
    @NonNull
    private RedisUtil redisUtil;
    public RedisCacheTemplate<SysUserEntity> userCache;
    @PostConstruct
    public void init() {
        userCache = new RedisCacheTemplate<SysUserEntity>(redisUtil, CacheKeyEnum.USER_ENTITY_KEY) {
            @Override
            public SysUserEntity getObjectFromDb(Object id) {
                ISysUserService userService = SpringUtil.getBean(ISysUserService.class);
                return userService.getById((Serializable) id);
            }
        };
    }
}

缓存Key以及过期时间

咱们能够通过一个枚举类来一致集中办理各个缓存的Key以及过期时间。以下是比如:

/**
 * @author valarchie
 */
public enum CacheKeyEnum {
    /**
     * Redis各类缓存集合
     */
    CAPTCHAT("captcha_codes:", 2, TimeUnit.MINUTES),
    LOGIN_USER_KEY("login_tokens:", 30, TimeUnit.MINUTES),
    RATE_LIMIT_KEY("rate_limit:", 60, TimeUnit.SECONDS),
    USER_ENTITY_KEY("user_entity:", 60, TimeUnit.MINUTES),
    ROLE_ENTITY_KEY("role_entity:", 60, TimeUnit.MINUTES),
    ROLE_MODEL_INFO_KEY("role_model_info:", 60, TimeUnit.MINUTES),
    ;
    CacheKeyEnum(String key, int expiration, TimeUnit timeUnit) {
        this.key = key;
        this.expiration = expiration;
        this.timeUnit = timeUnit;
    }
    private final String key;
    private final int expiration;
    private final TimeUnit timeUnit;
    public String key() {
        return key;
    }
    public int expiration() {
        return expiration;
    }
    public TimeUnit timeUnit() {
        return timeUnit;
    }
}

一致的运用门面

一般来说,咱们在项目中规划好缓存之后就能够让其他搭档写事务时直接调用了。可是让开发者去判断这个属于二级缓存仍是三级缓存的话,存在心智担负。咱们应该让开发者自然地从事务角度去选择某个缓存。比方他正在写部分相关的事务逻辑,就直接运用deptCache。 此刻咱们能够新建一个CacheCenter来一致按事务区分缓存。以下是比如:

/**
 * 缓存中心  供给全局拜访点
 * @author valarchie
 */
@Component
public class CacheCenter {
    public static AbstractGuavaCacheTemplate<String> configCache;
    public static AbstractGuavaCacheTemplate<SysDeptEntity> deptCache;
    public static RedisCacheTemplate<String> captchaCache;
    public static RedisCacheTemplate<LoginUser> loginUserCache;
    public static RedisCacheTemplate<SysUserEntity> userCache;
    public static RedisCacheTemplate<SysRoleEntity> roleCache;
    public static RedisCacheTemplate<RoleInfo> roleModelInfoCache;
    @PostConstruct
    public void init() {
        GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
        RedisCacheService redisCache = SpringUtil.getBean(RedisCacheService.class);
        configCache = guavaCache.configCache;
        deptCache = guavaCache.deptCache;
        captchaCache = redisCache.captchaCache;
        loginUserCache = redisCache.loginUserCache;
        userCache = redisCache.userCache;
        roleCache = redisCache.roleCache;
        roleModelInfoCache = redisCache.roleModelInfoCache;
    }
}

以上便是关于项目中多级缓存的完成。 如有缺乏恳请谈论指出。

全栈技术交流群:1398880