由于业务需要,最近写的一个项目不允许使用Redis等外部缓存,而且项目单节点部署,无需考虑数据互通的问题。因此手写一个基于ConcurrentMapCacheManager的缓存来使用。
该缓存仅支持过期删除淘汰策略,如果需要其他复杂的业务逻辑,可以考虑使用JCacheCacheManager
, EhCacheCacheManager
, CaffeineCacheManager
来实现。
最简单的缓存可以只用HashMap实现
Map<String, Object> cache = new HashMap<>();
但是单个HashMap实现会导致Key重复的问题,例如需要使用user_id来缓存用户的基本信息, 还有要用user_id缓存用户的登录信息。当然,可以通过user_id + 业务代号
的方式来避免冲突,但是当有删除所有登录信息的需求时处理起来就不那么方便了。
于是我们升级一下,使用HashMap嵌套HashMap来解决这个问题。
Map<String, Map<String, Object>> cache = new HashMap<>();
这种设计虽然能满足需求,但是get set方法显然处理起来不是那么优雅, 因此我们把目光转向org.springframework.cache.concurrent.ConcurrentMapCacheManager
这个类。
ConcurrentMapCacheManager
是ConcurrentMapCache
的管理类, 简单理解的话ConcurrentMapCache
可以看做是一个集装箱, 集装箱内装着我们所需要的货物(缓存对象), 而ConcurrentMapCacheManager
堆放集装箱的港口, 能够根据集装箱的编号查找出我们所需要的箱子.
ConcurrentMapCacheManager
包含两个构造方法, 我们只创建动态ConcurrentMapCacheManager
/**
* 构造一个动态的 ConcurrentMapCacheManager,在请
* 求它们时懒惰地创建缓存实例。
*/
public ConcurrentMapCacheManager() {}
/**
* 构造一个静态 ConcurrentMapCacheManager
* 仅管理指定缓存名称的缓存。
*/
public ConcurrentMapCacheManager(String... cacheNames) {
setCacheNames(Arrays.asList(cacheNames));
}
由于ConcurrentMapCacheManager
的使用需要实例化, 为了方便, 我们先对其封装一个单例管理类
/**
* 本地缓存缓存单例管理工具类
* 无需调用该类 请使用{@link LocalCache}来操作缓存
* @author xxx
* @version 1.0
* @date 2021/5/25 15:12
*/
public class LocalCacheManager {
private volatile static ConcurrentMapCacheManager cacheManager = null;
private static ConcurrentMapCacheManager cacheManager(){
if(null == cacheManager){
synchronized (LocalCacheManager.class){
if(null == cacheManager){
cacheManager = new ConcurrentMapCacheManager();
}
}
}
return cacheManager;
}
public static Cache getCache(String cacheName){
return cacheManager().getCache(cacheName);
}
}
至此, 准备工作就做好了, 接下来我们只需实现缓存的Get和Set方法就可以了.
简单的get和set是很好实现的, 我们只需要从LocalCacheManager
中取出我们想要操作的缓存集合, 然后再查询就可以了
public static void setObj(String cacheName, String key, Object value) {
Cache cache = LocalCacheManager.getCache(cacheName);
cache.put(key, value);
}
public static <T> T getObj(String cacheName, String key, Class<T> t) {
Cache cache = LocalCacheManager.getCache(cacheName);
return cache.get(key, t);
}
毕竟缓存是存储在内存中的, 内存空间寸土寸金, 且还有一部分数据需要及时从数据库中查询最新数据, 我们还需要实现过期清理功能.
此处我选择的策略是使用Jackson
(当然也可以用fastjson
等)将待缓存对象序列化为JSON字符串, 然后在JSON字符串前面拼上过期时间戳作为真正的Value存入缓存, 在获取时查验字符串, 如果过期就直接返回null
还有一种思路是单独使用一个数据结构存储key和过期时间, 此处不做实现.
为了方便, 我们封装一个缓存时间处理工具类
import java.util.Objects;
/**
* 缓存工具类时间快速计算
* @author wuhongwei
* @version 1.0
* @date 2021/5/25 15:48
*/
public class CacheTime {
/**
* 过期时间快速计算预置值
*/
public static final long SECOND = 1000L;
public static final long MINUTE = 60000L;
public static final long HOUR = 3600000L;
public static final long DAY = 86400000L;
public static final long WEEK = 604800000L;
public static final long YEAR = 31536000000L;
/**
* 编码分隔符
*/
private static char delimiter = '|';
/**
* 获取编码后的value 用于控制缓存过期时间
* @param value 缓存的value
* @param time 缓存有效时长
* @return
*/
public static String getEncodeValue(String value, Long time){
Long now = System.currentTimeMillis();
Long expireDate = now + time;
return new StringBuilder()
.append(expireDate)
.append(delimiter)
.append(value)
.toString();
}
/**
* 解码Value并判断是否过期
* @param encodeValue 缓存编码后的value
* @return
*/
public static String decodeValue(String encodeValue){
if(Objects.isNull(encodeValue)){
return null;
}
int index = encodeValue.indexOf(delimiter);
String timeStamp = encodeValue.substring(0, index);
if(Long.parseLong(timeStamp) > System.currentTimeMillis()){
String value = encodeValue.substring(index + 1);
return value;
}
return null;
}
}
将上述逻辑写入到get和set方法中, 过期时间功能就实现了
public static void setObj(String cacheName, String key, Object value, Long milliseconds) throws JsonProcessingException {
Cache cache = LocalCacheManager.getCache(cacheName);
synchronized (cache){
// JSONUtil是我对Jackson ObjectMapper 的一个封装
String valueJson = JSONUtil.toJsonString(value);
valueJson = CacheTime.getEncodeValue(valueJson, milliseconds);
cache.put(key, valueJson);
}
}
public static <T> T getObj(String cacheName, String key, Class<T> t) throws JsonProcessingException {
Cache cache = LocalCacheManager.getCache(cacheName);
String encodeValue = cache.get(key, String.class);
String value = CacheTime.decodeValue(encodeValue);
if(StringUtils.isEmpty(value)){
cache.evict(key);
return null;
}
// JSONUtil是我对Jackson ObjectMapper 的一个封装
return JSONUtil.parseJson(value, t);
}
此时该缓存基本已经实现了, 但是对于只访问了一次的数据, 显然不能有效的清除缓存, 因此还需要一个定时任务, 定时任务需要遍历所有的缓存集合及其中的所有缓存数据, 查找出过期缓存并删除.
为了便于遍历, 我们添加一个Set对象存储所有的缓存集合名称, 至此缓存工具类编写完成
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.cache.Cache;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Set;
/**
* 本地缓存实现
* 内存简易缓存 重启或宕机会丢失数据
* 不允许使用长效缓存
*/
public class LocalCache {
public static Set<String> cacheNames = new HashSet<>();
public static void setObj(String cacheName, String key, Object value, Long milliseconds) throws JsonProcessingException {
Cache cache = LocalCacheManager.getCache(cacheName);
synchronized (cache){
String valueJson = JSONUtil.toJsonString(value);
valueJson = CacheTime.getEncodeValue(valueJson, milliseconds);
cache.put(key, valueJson);
// 添加缓存集合名称
cacheNames.add(cacheName);
}
}
public static <T> T getObj(String cacheName, String key, Class<T> t) throws JsonProcessingException {
Cache cache = LocalCacheManager.getCache(cacheName);
String encodeValue = cache.get(key, String.class);
String value = CacheTime.decodeValue(encodeValue);
if(StringUtils.isEmpty(value)){
cache.evict(key);
return null;
}
return JSONUtil.parseJson(value, t);
}
public static Cache getCache(String cacheName){
Cache cache = LocalCacheManager.getCache(cacheName);
return cache;
}
}
定时处理任务编写比较简单, 这里只贴出代码
@Scheduled(cron = "0 0/10 * * * ?")
private void cleanCache(){
log.debug("开始清理缓存->>>>>>>>>>");
Set<String> cacheNames = LocalCache.cacheNames;
for(String cacheName : cacheNames){
Cache c = LocalCache.getCache(cacheName);
if(c == null){
continue;
}
synchronized(c){
ConcurrentMap<Object, Object> realCache = (ConcurrentMap<Object, Object>)c.getNativeCache();
for (Map.Entry<Object, Object> entry : realCache.entrySet()){
String key = entry.getKey().toString();
String encodeValue = entry.getValue().toString();
String value = CacheTime.decodeValue(encodeValue);
if(StringUtils.isEmpty(value)){
log.debug("删除缓存->>>>>>>>>>" + cacheName + "->>>>>>>" + key);
c.evict(key);
}
}
}
}
}
这种简单缓存实现简单, 便于使用, 但是功能单一, 且性能较差, 仅适合于部分场景和学习