无法使用redis或其他中间件时,Java本地缓存实现

Posted Ps_Q

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无法使用redis或其他中间件时,Java本地缓存实现相关的知识,希望对你有一定的参考价值。

无法使用redis或其他中间件时,Java本地缓存实现

在项目遇到一个问题,需要保存一些验证码,但由于是老项目,并且没有基于maven,因此导包比较麻烦。好在项目基于spring,而spring中的org.springframework.cache.Cache相关类恰好可以解决。真是不幸中的万幸。

创建了三个类,一个缓存工具类,一个管理缓存工具类,一个过期时间处理类。

当时并不具备导入外部工具的条件,只有基于spring4.0+本本,连springboot都不具备,当然这是在我本地代码重现当时的情况,增加了Ali JSON处理工具,hutool工具pom如下。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>4.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.49</version>
        </dependency>

LocalCache 类的内容

public class LocalCache 
    /**
     * 当前自动清理任务状态,true为暂未启动,false为已经启动
     */
    private static volatile AtomicBoolean state = new AtomicBoolean(true);

    /**
     * 实时缓存名
     */
    private static volatile Set<String> cacheNames = Collections.synchronizedSet(new HashSet<>());

    /**
     * @param cacheName    缓存名
     * @param key          key
     * @param value        value
     * @param milliseconds 过期时间
     */
    public static void put(String cacheName, String key, Object value, Long milliseconds) 
        Cache cache = LocalCacheManager.getCache(cacheName);
        synchronized (cache) 
            String valueJson = JSON.toJSONString(value);
            valueJson = CacheTime.getEncodeValue(valueJson, milliseconds);
            cache.put(key, valueJson);
            cacheNames.add(cacheName);
        
        if (state.get()) 
            autoClearExpiredCache();
        
    

    public static <T> T get(String cacheName, String key, Class<T> t) 
        Cache cache = LocalCacheManager.getCache(cacheName);
        String encodeValue = cache.get(key, String.class);
        String value = CacheTime.decodeValue(encodeValue);
        if (StrUtil.isBlank(value)) 
            cache.evict(key);
            return null;
        
        return JSON.parseObject(value, t);
    

    public void remove(String cacheName, String key) 
        Cache cache = LocalCacheManager.getCache(cacheName);
        synchronized (cache) 
            cache.evict(key);
        
    

    private static void autoClearExpiredCache() 
        new Thread(() -> 
            while (cacheNames.size() > 0) 
                state.set(false);
                try 
                    Thread.sleep(30000L);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                Set<String> set = LocalCache.cacheNames;
                set.forEach(cacheName -> 
                    Cache cache = LocalCacheManager.getCache(cacheName);
                    if (Objects.nonNull(cache)) 
                        synchronized (cache) 
                            ConcurrentHashMap store = (ConcurrentHashMap) cache.getNativeCache();
                            if (CollUtil.isEmpty(store)) 
                                cacheNames.remove(cacheName);
                             else 
                                store.forEach((key, value) -> 
                                    String decodeValue = CacheTime.decodeValue((String) value);
                                    if (StrUtil.isBlank(decodeValue)) 
                                        cache.evict(key);
                                    
                                );
                            
                        
                     else 
                        cacheNames.remove(cacheName);
                    
                );
            
            // 释放空间 初始化任务状态
            LocalCacheManager.clear();
            state.set(true);
        ).start();
    

CacheTime

public class CacheTime 
    /**
     * 编码分隔符
     */
    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())
            return encodeValue.substring(index + 1);
        
        return null;
    

LocalCacheManager

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);
    

    public static void clear() 
        cacheManager = null;
    

基本上就是实现了一个类似于redis的工具吧。借鉴了老哥的代码。改成了一个单独线程自动清理过期缓存,由于我使用这段代码的时候只在特殊场景,大部分情况其实缓存都是空的,所有自动清理任务在缓存为空的情况是不运行的,包括缓存工具类的静态变量也是null。节约了一些资源。

https://blog.csdn.net/qq_38634643/article/details/118785050

但在不考虑缓存过期的情况,也可以不需要整这么些。

直接使用HashMap即可解决问题

public class LRUCache extends LinkedHashMap 

    /**
     * 可重入读写锁,保证并发读写安全性
     */
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    /**
     * 缓存大小限制
     */
    private int maxSize;

    public LRUCache(int maxSize) 
        super(maxSize + 1, 1.0f, true);
        this.maxSize = maxSize;
    

    @Override
    public Object get(Object key) 
        readLock.lock();
        try 
            return super.get(key);
         finally 
            readLock.unlock();
        
    

    @Override
    public Object put(Object key, Object value) 
        writeLock.lock();
        try 
            return super.put(key, value);
         finally 
            writeLock.unlock();
        
    

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) 
        return this.size() > maxSize;
    


当然也有其他非常方便的工具类

google.guava

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>
public class GuavaCacheMain 

    public static void main(String[] args) throws Exception 
        //创建guava cache
        Cache<String, String> loadingCache = CacheBuilder.newBuilder()
                //cache的初始容量
                .initialCapacity(5)
                //cache最大缓存数
                .maximumSize(10)
                //设置写缓存后n秒钟过期
                .expireAfterWrite(17, TimeUnit.SECONDS)
                //设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                .build();
        String key = "key";
        // 往缓存写数据
        loadingCache.put(key, "v");

        // 获取value的值,如果key不存在,调用collable方法获取value值加载到key中再返回
        String value = loadingCache.get(key, new Callable<String>() 
            @Override
            public String call() throws Exception 
                return getValueFromDB(key);
            
        );

        // 删除key
        loadingCache.invalidate(key);
    

    private static String getValueFromDB(String key) 
        return "v";
    

Caffeine (强力推荐) 在spring 5.0之后是默认的,是基于Guava在jdk1.8后重写的,性能最佳

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.8.0</version>
</dependency>

或者

<dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.6.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.10.RELEASE</version>
        </dependency>
public class CaffeineCacheTest 

    public static void main(String[] args) throws Exception 
        //创建guava cache
        Cache<String, String> loadingCache = Caffeine.newBuilder()
                //cache的初始容量
                .initialCapacity(5)
                //cache最大缓存数
                .maximumSize(10)
                //设置写缓存后n秒钟过期
                .expireAfterWrite(17, TimeUnit.SECONDS)
                //设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                .build();
        String key = "key";
        // 往缓存写数据
        loadingCache.put(key, "v");

        // 获取value的值,如果key不存在,获取value后再返回
        String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);

        // 删除key
        loadingCache.invalidate(key);
    

    private static String getValueFromDB(String key) 
        return "v";
    

Encache 是Hibernate默认的

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.8.0</version>
</dependency>
public class EncacheMain 

    public static void main(String[] args) throws Exception 
        // 声明一个cacheBuilder
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("encacheInstance", CacheConfigurationBuilder
                        //声明一个容量为20的堆内缓存
                        .newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20)))
                .build(true);
        // 获取Cache实例
        Cache<String,String> myCache =  cacheManager.getCache("encacheInstance", String.class, String.class);
        // 写缓存
        myCache.put("key","v");
        // 读缓存
        String value = myCache.get("key");
        // 移除换粗
        cacheManager.removeCache("myCache");
        cacheManager.close();
    


以上是关于无法使用redis或其他中间件时,Java本地缓存实现的主要内容,如果未能解决你的问题,请参考以下文章

Redis+Caffeine两级缓存,让访问速度纵享丝滑

SpringBoot进阶之缓存中间件Redis

高性能的本地缓存方案选型,看这篇就够了!

高性能的本地缓存方案选型,看这篇就够了!

微服务连接不上redis会直接访问数据库吗

DELPHI开发和使用REDIS