将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粒度的失效时间,所以需要在父类中维护失效时间,子类只是负责通过组合的方式提供缓存介质。
- 每次get的时候,判断value是否失效;
- 为每一个缓存开启定时,时刻去清除缓存中已经失效的value;
以本地缓存为例,缓存实体为static静态属性。即无论缓存类被new多少次,均共有一个缓存实体。
难点:清除缓存的子线程开启时机,我们需要放在抽象类的静态方法中,但是这样每一次new 子类对象时,均要开启一个定时任务。但是我们的缓存实体是项目全局共享,那么会导致多个定时任务去清空一个缓存介质的现象。
思路:若定时任务的启动,只是在类第一次被创建对象时启动,后续该类无论创建多少次均不会启动。
解决方案:使用static ConcurrentHashMap
的computeIfAbsent
实现。因为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);
}
}