美文网首页
将Cache操作模板化—论:如何实现key粒度失效时间的缓存

将Cache操作模板化—论:如何实现key粒度失效时间的缓存

作者: 小胖学编程 | 来源:发表于2021-07-05 16:10 被阅读0次

    cache操作本身就具有模板化,即判断value是否存在,加内存锁防止缓存穿透,然后双重校验判断value是否存在,最终去调用真正逻辑获取value并且维护缓存。那么可以借助接口的default方法来定义模板。

    1. 接口类

    接口类中定义了模板方法,维护缓存时,可以直接使用上面的模板方法。
    并且采用jdk8提供的Supplier<T>Function<T,R>类。类似于策略模式,将具体的逻辑传入。

    import java.lang.reflect.Type;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.function.Function;
    import java.util.function.Supplier;
    
    
    public interface CacheManager {
    
        /**
         * 失效时间的比例。
         * 缓存的失效时间=申请的有效时间*expireRate
         */
        double expireRate = 0.75;
    
        /**
         * 内存锁的缓存类
         */
        Map<String, Object> locks = new ConcurrentHashMap<>(8);
    
        /**
         * 填充缓存的值
         *
         * @param key   缓存的key
         * @param value 缓存的value
         * @param time  失效时间,单位ms
         */
        void put(String key, String value, long time);
    
    
        /**
         * 获取缓存的的值
         *
         * @param key 缓存的key
         * @return 缓存的值
         */
        String get(String key);
    
    
        /**
         * 移除缓存的的值
         *
         * @param key 缓存的key
         */
        void remove(String key);
    
    
        /**
         * 在缓存中获取值
         *
         * @param supplier 回调的逻辑代码
         * @param key      缓存的key
         * @param time     失效时间,ms
         * @param type     缓存的值反序列化的类型。
         *                 {@code Type type = new TypeReference<User>(){}.getType();}
         * @param <T>
         * @return
         */
        default <T> T getInCache(Supplier<T> supplier, String key, long time, Type type) {
            T result = null;
            String v = getInCache(supplier, key, time);
            if (v != null) {
                result = JSON.parseObject(v, type);
            }
            return result;
        }
    
        /**
         * 在缓存中获取值
         *
         * @param supplier      回调的逻辑代码
         * @param key           缓存的key
         * @param genExpireTime 根据回调逻辑的返回值来计算失效时间
         * @param type          缓存的值反序列化的类型。
         *                      {@code Type type = new TypeReference<User>(){}.getType();}
         * @param <T>
         * @return
         */
        default <T> T getInCache(Supplier<T> supplier, String key, Function<T, Long> genExpireTime, Type type) {
            T result = null;
            String v = getInCache(supplier, key, genExpireTime);
            if (v != null) {
                result = JSON.parseObject(v, type);
            }
            return result;
        }
    
    
        /**
         * 在缓存中获取字符串信息
         *
         * @param supplier 回调的逻辑代码
         * @param key      缓存的key
         * @param time     失效时间,ms
         * @return 缓存中存储的字符串信息
         */
        default <T> String getInCache(Supplier<T> supplier, String key, long time) {
            //获取锁
            Object lock = CollectionUtil.computeIfAbsent(locks, key, k -> new Object());
            //获取cache的key
            T result;
            //缓存中获取值
            String v = get(key);
            if (v == null) {
                synchronized (lock) {
                    v = get(key);
                    if (v == null) {
                        result = supplier.get();
                        if (result != null) {
                            v = JSON.toJSONString(result);
                            put(key, v, Math.round(time * expireRate));
                        }
                    }
                }
            }
            return v;
        }
    
        /**
         * 在缓存中获取字符串信息
         *
         * @param supplier      回调的逻辑代码
         * @param key           缓存的key
         * @param genExpireTime 根据回调逻辑的返回值来计算失效时间
         * @return 缓存中存储的字符串信息
         */
        default <T> String getInCache(Supplier<T> supplier, String key, Function<T, Long> genExpireTime) {
            //获取cache的key
            T result;
            //缓存中获取值
            String v = get(key);
            if (v == null) {
                //获取锁
                Object lock = CollectionUtil.computeIfAbsent(locks, key, k -> new Object());
                synchronized (lock) {
                    v = get(key);
                    if (v == null) {
                        result = supplier.get();
                        if (result != null) {
                            v = JSON.toJSONString(result);
                            Long expireTime = genExpireTime.apply(result);
                            if (expireTime == null) {
                                throw new RuntimeException("expireTime不能为空!");
                            }
                            put(key, v, Math.round(expireTime * expireRate));
                        }
                    }
                }
            }
            return v;
        }
    
    }
    

    工具类:

    public class CollectionUtil {
        /**
         * 解决
         * JDK1.8的ConcurrentHashMap提供的computeIfAbsent性能问题
         * https://www.jianshu.com/p/6c294df2b88d
         *
         */
        public static <K, V> V computeIfAbsent(Map<K, V> concurrentHashMap, K key, Function<? super K, ? extends V> mappingFunction) {
            V v = concurrentHashMap.get(key);
            if (v != null) {
                return v;
            }
            return concurrentHashMap.computeIfAbsent(key, mappingFunction);
        }
    }
    

    2. 实现类

    2.1 Redis的实现类

    Reids的实现类采用的是String的结构进行缓存。实现简单。

    @Component("redisCacheManager")
    public class SealRedisCacheManager implements CacheManager {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        /**
         * 填充缓存
         * @param key   缓存的key
         * @param value 缓存的value
         * @param time  失效时间,单位ms
         */
        @Override
        public void put(String key, String value, long time) {
            stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.MILLISECONDS);
        }
    
        /**
         * 获取缓存的值
         * @param key 缓存的key
         * @return
         */
        @Override
        public String get(String key) {
            return stringRedisTemplate.opsForValue().get(key);
        }
    
        /**
         * 移除缓存
         * @param key 缓存的key
         */
        @Override
        public void remove(String key) {
            stringRedisTemplate.delete(key);
        }
    }
    

    2.2 自己维护失效时间的抽象类

    因为一些缓存没有key粒度的失效时间,所以需要在父类中维护失效时间,子类只是负责通过组合的方式提供缓存介质。

    • 每次get的时候,判断value是否失效;
    • 为每一个缓存开启定时,时刻去清除缓存中已经失效的value;

    以本地缓存为例,缓存实体为static静态属性。即无论缓存类被new多少次,均共有一个缓存实体。

    难点:清除缓存的子线程开启时机,我们需要放在抽象类的静态方法中,但是这样每一次new 子类对象时,均要开启一个定时任务。但是我们的缓存实体是项目全局共享,那么会导致多个定时任务去清空一个缓存介质的现象。

    思路:若定时任务的启动,只是在第一次被创建对象时启动,后续该类无论创建多少次均不会启动。

    解决方案:使用static ConcurrentHashMapcomputeIfAbsent实现。因为ConcurrentHashMap是静态的,所以也是全局共享。computeIfAbsent方法会判断key的value是否存在,若不存在,那么取维护,若存在,直接返回。这也就可以实现,只有类第一次被创建时,定时任务才会被开启。

    代码实现:

    @Slf4j
    public abstract class AbstractMaintenanceExpiredCacheManager implements CacheManager {
    
        //初始化定时器
        private static ScheduledExecutorService scheduler =
                Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("cache clear", true));
    
        /**
         * 保证对象无论创建多少次,也只会在第一次创建的时候,开启定时任务
         */
        private static Map<String, Object> locks = new ConcurrentHashMap<>();
    
    
        public AbstractMaintenanceExpiredCacheManager() {
            /**
             * 无论创建多少次缓存对象,定时器也只能开启一个任务,故使用该配置。
             */
            CollectionUtil.computeIfAbsent(locks, this.getClass().getName(), k -> {
                afterPropertiesSet();
                return new Object();
            });
    
        }
    
        /**
         * 填充数据
         *
         * @param key       请求key
         * @param cacheData 缓存带失效时间的对象
         */
        public abstract void putCacheData(String key, CacheData cacheData);
    
        /**
         * 存储数据
         *
         * @param key 请求key
         */
        public abstract CacheData getCacheData(String key);
    
        /**
         * 将集合转换为Map
         *
         * @return 转换为ConcurrentHashMap对象
         */
        public abstract ConcurrentMap<String, CacheData> asMap();
    
        /**
         * 父类对象
         *
         * @param key   缓存的key
         * @param value 缓存的value
         * @param time  失效时间,单位ms
         */
        @Override
        public void put(String key, String value, long time) {
            putCacheData(key, new CacheData(value, System.currentTimeMillis() + time));
        }
    
        /**
         * 获取缓存的值
         *
         * @param key 缓存的key
         * @return 缓存的值
         */
        @Override
        public String get(String key) {
            CacheData cacheData = getCacheData(key);
            String value = null;
            //校验数据
            if (cacheData != null) {
                //数据过期,手动移除
                if (System.currentTimeMillis() >= cacheData.expire) {
                    remove(key);
                    value = null;
                } else {
                    value = cacheData.getValue();
                }
            }
            return value;
        }
    
        public void afterPropertiesSet() {
            scheduler.scheduleAtFixedRate(() -> {
                //定时清空的机制
                ConcurrentMap<String, CacheData> map = asMap();
                map.forEach((k, v) -> {
                    //判断是否失效
                    if (System.currentTimeMillis() >= v.expire) {
                        remove(k);
                    }
                });
    
            }, 0, SealMathUtils.randomLongIfRange("4000-5000"), TimeUnit.MILLISECONDS);
        }
    
    
        @Getter
        static class CacheData {
    
            /**
             * 存储的值
             */
            private String value;
    
            /**
             * 失效时间戳,单位ms
             */
            private long expire;
    
            public CacheData(String value, long expire) {
                this.value = value;
                this.expire = expire;
            }
        }
    
    }
    

    子类代码实现:

    public class GoogleGuavaCacheManager extends AbstractMaintenanceExpiredCacheManager {
    
        /**
         * 注意此处为静态属性,即无论GoogleGuavaCacheManager被创建了多少次,此处依旧是一个缓存
         */
        private static Cache<String, CacheData> cache = CacheBuilder.newBuilder().
                maximumSize(500).build();
    
        @Override
        public void putCacheData(String key, CacheData cacheData) {
            cache.put(key, cacheData);
        }
    
        @Override
        public CacheData getCacheData(String key) {
            return cache.getIfPresent(key);
        }
    
        @Override
        public ConcurrentMap<String, CacheData> asMap() {
            return cache.asMap();
        }
    
        @Override
        public void remove(String key) {
            cache.invalidate(key);
        }
    }
    

    相关文章

      网友评论

          本文标题:将Cache操作模板化—论:如何实现key粒度失效时间的缓存

          本文链接:https://www.haomeiwen.com/subject/fghiultx.html