5、缓存组件设计

5.1 整体架构

先上一张类图,这张类图包含了Dawn缓存架构设计的整体结构:

查询java服务 占用的缓存 java从缓存中查询数据_redis

其中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代码简单,功能也简单,但对于一些简单轻量的缓存,却也足够使用了。

END(4)