5、缓存组件设计
5.1 整体架构
先上一张类图,这张类图包含了Dawn缓存架构设计的整体结构:
其中DawnCache是一个缓存组件的通用接口,包含了获取缓存、设置缓存、清除缓存三个方法,而DawnRedisCache是它的Redis实现,DawnEhcacheCache是它的Ehcache实现,DawnSimpleCache是它的基于Java Map 的实现。三种缓存组件分别适用不同情况下的使用。
DawnCacheFactory是一个工厂类,用来根据传入的不同的缓存类型生成对应的缓存组件。
缓存类型采用了枚举定义:
@Getter
@AllArgsConstructor
public enum DawnCacheType {
EHCACHE("ehcache"),
REDIS("redis"),
SIMPLE("simple"),
DEFAULT("default"),
;
private final String name;
public static DawnCacheType getCacheTypeByName(String name) {
return Arrays.stream(DawnCacheType.values())
.filter(x -> x.getName().equals(name)).findFirst().orElse(null);
}
}
其中的成员分别对应上述的缓存组件,DEFAULT是根据外部配置设置的默认的缓存组件类型,通过修改yml配置中的DEFAULT可以快速的修改应用使用的缓存组件。
目前的三个缓存组件,从开销上来说,SimpleCache最低,因为它是基于Java Map的实现,主要使用的是Java的堆上内存;EhcacheCache其次,它可以使用到堆外内存,并且还可以将缓存持久化到硬盘;Redis最高,它需要先部署Redis服务,通信时也需要更多的IO开销。
从速度来说,SimpleCache最快,EhcacheCache其次,RedisCache最慢。
从功能强大性来说,RedisCache最强,Redis本身的功能就较为强大,各种不同的数据结构都可以使用,不仅支持持久化,也可以搭建分布式服务,通过主从备份、哨兵节点实现高可用。Ehcache其次,SimpleCache最为简单,功能也最弱。
从使用上来说,SimpleCache适用于那些使用频次极高而数据量不大的数据,例如字典值、型号、系统配置等;Ehcache适用于那些常见的数据查询,如常见的分页查询、图表查询;Redis适用于那些需要大空间、分布式查询的数据。
5.2 缓存工厂类
public class DawnCacheFactory {
@Resource
private DawnProperties dawnProperties;
@Resource
private RedisUtil redisUtil;
private static Map<DawnCacheType, DawnCache> CACHE_MAP;
/**
* 根据缓存类型获取缓存组件,缓存组件的实例会被缓存到CACHE_MAP
* synchronized 同步是为了CACHE_MAP在并发下只被初始化一次,并且保证缓存组件实例不会被重复的初始化
*
* @param dawnCacheType @see {@link DawnCacheType}
* @return 缓存组件
*/
public synchronized DawnCache getCache(DawnCacheType dawnCacheType) {
if (CACHE_MAP == null) {
CACHE_MAP = new ConcurrentHashMap<>(DawnCacheType.values().length);
}
DawnCache dawnCache = CACHE_MAP.get(dawnCacheType);
if (dawnCache == null) {
dawnCache = createCacheByType(dawnCacheType);
CACHE_MAP.put(dawnCacheType, dawnCache);
}
return dawnCache;
}
/**
* 根据缓存类型获取缓存组件
*
* @param dawnCacheType @see {@link DawnCacheType}
* @return 缓存组件
*/
private DawnCache createCacheByType(DawnCacheType dawnCacheType) {
return switch (dawnCacheType) {
case DEFAULT -> getDawnDefaultCache();
case EHCACHE -> new DawnEhcacheCache();
case REDIS -> new DawnRedisCache(redisUtil);
case SIMPLE -> new DawnSimpleCache();
};
}
/**
* 根据配置中配置的默认缓存类型,生成缓存组件的实例
*
* @return 默认缓存组件
*/
private DawnCache getDawnDefaultCache() {
String defaultCacheType = dawnProperties.getBase().getDefaultCacheType();
if (defaultCacheType == null) {
defaultCacheType = DawnBaseConfig.getInstance().getDawn().getConfig().getDefaultCacheType();
}
DawnCacheType dawnCacheType = DawnCacheType.getCacheTypeByName(defaultCacheType);
if (dawnCacheType == null) {
throw new RuntimeException("无法找到该缓存类型:" + defaultCacheType);
}
if (dawnCacheType == DawnCacheType.DEFAULT) {
throw new RuntimeException("默认缓存类型不能是default");
}
return getCache(dawnCacheType);
}
}
getCache
根据缓存类型获取缓存组件,缓存组件的实例会被缓存到CACHE_MAP,synchronized 同步是为了CACHE_MAP在并发下只被初始化一次,并且保证缓存组件实例不会被重复的初始化。
createCacheByType
据缓存类型返回对应的缓存组件,这边使用了Java的新的switch语法,使代码看起来更加的简洁。
getDawnDefaultCache
根据配置中配置的默认缓存类型,生成缓存组件的实例,它要求配置的缓存组件不能是DEFAULT,并且当组件不存在时会抛出异常。
5.3 EhcacheCache组件
EhcacheCache组件的代码很简洁,主要就是调用预先封装好的Ehcache2Cache这个Ehcache2缓存工具类来执行缓存的获取、设置、删除。需要注意,因为Ehcache的默认缓存时间单位是秒,所以它不支持毫秒及以下的缓存过期时间单位,当设置了毫秒的过期时间单位时,它的缓存时间将会是0,从而会永不过期!
@Slf4j
public class DawnEhcacheCache implements DawnCache {
@Override
public Pair<Boolean, Optional<?>> getFromCache(String key) {
return Ehcache2Cache.getValueWithState(Ehcache2Caches.COMMON_CACHE, key);
}
@Override
public int deleteCache(String keyPrefix) {
int count = 0;
@SuppressWarnings("unchecked")
List<String> keys = (List<String>) Ehcache2Cache.getKeys(Ehcache2Caches.COMMON_CACHE);
for (String key : keys) {
if (key.startsWith(keyPrefix)) {
Ehcache2Cache.removeElement(Ehcache2Caches.COMMON_CACHE, key);
log.info("EHCACHE缓存清除:" + key);
count ++;
}
}
return count;
}
@Override
public void setCache(String key, Object value, long timeToLive, TimeUnit timeUnit) {
Ehcache2Cache.setValue(Ehcache2Caches.COMMON_CACHE, key, value, (int) timeUnit.toSeconds(timeToLive));
}
}
Pair<Boolean, Optional<?>> getFromCache(String key)
获取缓存除了获取缓存的内容之外,还会返回缓存中是否存在这个Key,这是为了应对缓存穿透,具体的设计可以参看java 实现的数据查询缓存通用模型——缓存模型核心AOP实现(3) 4.2部分。
5.4 RedisCache组件
RedisCache组件主要也是依托于RedisUtil这个缓存工具类来实现的,代码很简单,就不再赘述:
@Slf4j
public class DawnRedisCache implements DawnCache {
private final RedisUtil redisUtil;
public DawnRedisCache(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
@Override
public Pair<Boolean, Optional<?>> getFromCache(String key) {
return redisUtil.getObjectWithState(key);
}
@Override
public int deleteCache(String keyPrefix) {
int count = 0;
List<String> keys = redisUtil.deleteKeysWithPre(keyPrefix);
for (String key : keys) {
log.info("REDIS缓存清除:" + key);
count ++;
}
return count;
}
@Override
public void setCache(String key, Object value, long timeToLive, TimeUnit timeUnit) {
redisUtil.setObject(key, value, timeToLive, timeUnit);
}
}
5.5 SimpleCache组件
SimpleCache组件使用 Java 内置的 MAP 实现的简易缓存,适用于一些轻量级缓存:
@Slf4j
public class DawnSimpleCache implements DawnCache {
/**
* 保存缓存数据
*/
private final Map<String, Object> cache;
/**
* 保存缓存对应的过期时间
*/
private final Map<String, Long> cacheExpireRecord;
public DawnSimpleCache() {
cache = new ConcurrentHashMap<>();
cacheExpireRecord = new ConcurrentHashMap<>();
}
@Override
public Pair<Boolean, Optional<?>> getFromCache(String key) {
boolean cacheContainsKey = true;
Object value = null;
Long expireTime = cacheExpireRecord.get(key);
// 如果存在这个Key
if (expireTime != null) {
// 如果数据已经过期,此处使用了延迟删除,只有查询发现过期了才会删除
// 0L是特殊的,它代表永不过期
if (expireTime != 0L && expireTime < System.currentTimeMillis()) {
log.info("SIMPLE CACHE缓存过期:" + key);
deleteCache(key);
cacheContainsKey = false;
} else {
// 从缓存中获取数据
value = cache.get(key);
}
} else {
cacheContainsKey = false;
}
return Pair.of(cacheContainsKey, Optional.ofNullable(value));
}
@Override
public int deleteCache(String keyPrefix) {
Set<String> keys = new HashSet<>();
// 先查到以这个前缀开头的所有Key
cache.forEach((k, v) -> {
if (k.startsWith(keyPrefix)) {
keys.add(k);
}
});
// 遍历这些Key,依次清除
keys.forEach(key -> {
cacheExpireRecord.remove(key);
cache.remove(key);
log.info("SIMPLE CACHE缓存清除:" + key);
});
return keys.size();
}
@Override
public void setCache(String key, Object value, long timeToLive, TimeUnit timeUnit) {
cache.put(key, value);
// 记录失效时间
if (timeToLive <= 0L) {
cacheExpireRecord.put(key, 0L);
} else {
cacheExpireRecord.put(key, System.currentTimeMillis() + timeUnit.toMillis(timeToLive));
}
}
}
其中cacheExpireRecord这个Map的用途就是用来保存缓存的过期时间,代码中使用了延迟删除,只有查询发现过期了才会删除
,0L是特殊的,它代表永不过期。
这个SimpleCache代码简单,功能也简单,但对于一些简单轻量的缓存,却也足够使用了。