Spring实现一个基于ConcurrentMapCacheManager的简单缓存

写在前面

由于业务需要,最近写的一个项目不允许使用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这个类。

ConcurrentMapCacheManagerConcurrentMapCache的管理类, 简单理解的话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);
                    }
                }
            }
        }
    }

总结

这种简单缓存实现简单, 便于使用, 但是功能单一, 且性能较差, 仅适合于部分场景和学习

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值