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

2021-07-05  本文已影响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粒度的失效时间,所以需要在父类中维护失效时间,子类只是负责通过组合的方式提供缓存介质。

以本地缓存为例,缓存实体为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);
    }
}
上一篇下一篇

猜你喜欢

热点阅读