java内存缓存

发布于:2025-08-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

 我们在项目中会经常使Redis和Memcache,但是简单项目就没必要使用专门的缓存框架来增加系统的复杂性。用Java代码逻辑就能实现内存级别的缓存。

1.定时任务线程池

使用ScheduledExecutorService结合ConcurrentHashMap,如果你使用的是ConcurrentHashMap,你可以结合使用ScheduledExecutorService来定期检查并清理过期的条目。

public class ExpiringMap<K, V> {
    private final ConcurrentHashMap<K, ExpiringValue> map = new ConcurrentHashMap<>();
    private final long expirationTime; // 毫秒
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
 
    public ExpiringMap(long expirationTime) {
        this.expirationTime = expirationTime;
        // 安排一个任务定期检查并清理过期条目
        scheduler.scheduleAtFixedRate(this::cleanUp, expirationTime, expirationTime, TimeUnit.MILLISECONDS);
    }
 
    public void put(K key, V value) {
        map.put(key, new ExpiringValue(value, System.currentTimeMillis() + expirationTime));
    }
 
    private void cleanUp() {
        long currentTime = System.currentTimeMillis();
        map.entrySet().removeIf(entry -> entry.getValue().expirationTime < currentTime);
    }
 
    static class ExpiringValue {
        final V value;
        final long expirationTime;
 
        ExpiringValue(V value, long expirationTime) {
            this.value = value;
            this.expirationTime = expirationTime;
        }
    }
}

2. java.time.Instant

和方式一类似,使用java.time.Instant来手动管理过期时间,并结合一个后台线程来定期清理。

public class ExpiringMapWithManualCleanup<K, V> {
    private final Map<K, Entry<V>> map = new ConcurrentHashMap<>(); 
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private final long expirationTime; // 毫秒
 
    public ExpiringMapWithManualCleanup(long expirationTime) {
        this.expirationTime = expirationTime;
        scheduler.scheduleAtFixedRate(this::cleanUp, expirationTime, expirationTime, TimeUnit.MILLISECONDS);
    }
 
    public void put(K key, V value) {
        map.put(key, new Entry<>(value, Instant.now().plusMillis(expirationTime)));
    }
 
    private void cleanUp() {
        Instant now = Instant.now();
        map.entrySet().removeIf(entry -> entry.getValue().expirationTime.isBefore(now));
    }
 
    static class Entry<V> {
        final V value;
        final Instant expirationTime;
        Entry(V value, Instant expirationTime) {
            this.value = value;
            this.expirationTime = expirationTime;
        }
    }
}

3. 使用第三方库

 3.1 ExpiringMap使用

引入依赖

        <dependency>
            <groupId>net.jodah</groupId>
            <artifactId>expiringmap</artifactId>
            <version>0.5.10</version>
        </dependency>
    /**
     * ① maxSize:Map存储的最大值,类似队列,容量固定,当操作map容量超出限制时,最开始的元素就会依次过期,只保留最新的;
     * ② expiration:过期时间;
     * ③ expirationListener:过期监听,当条目过期时,将同步调用过期侦听器,并且在侦听器完成之前,
     *  将阻止对映射的写入操作。还可以在单独的线程池中配置和调用异步过期侦听器,而不会阻塞映射操作;
     * ④ expirationPolicy:过期策略,包括 ExpirationPolicy.ACCESSED 和 ExpirationPolicy.CREATED 两种;
     *      1)ExpirationPolicy.ACCESSED :每进行一次访问,过期时间就会自动清零,重新计算;
     *      2)ExpirationPolicy.CREATED:在过期时间内重新 put 值的话,过期时间会清理,重新计算;
     * ⑤ variableExpiration:可变过期,条目可以具有单独可变的到期时间和策略:
     */
    public static  ExpiringMap<String, String> map = ExpiringMap.builder()
            .maxSize(1000)
            .expiration(2, TimeUnit.HOURS)
            .variableExpiration()
            .expirationPolicy(ExpirationPolicy.ACCESSED)
            .expirationListener((key, value) -> {
                System.out.println("SseEmitter已过期,key:"+ key);
            })
            .build();

使用

//为单个条目指定到期策略:
        map.put("1", "张三", ExpirationPolicy.CREATED);
        map.put("2", "李四", ExpirationPolicy.ACCESSED);

        //variableExpiration 可变过期 条目可以具有单独可变的到期时间和策略:
        map.put("3", "王五", ExpirationPolicy.ACCESSED, 5, TimeUnit.MINUTES);

        //过期时间和策略也可以即时更改:
        map.setExpiration("1", 5, TimeUnit.MINUTES);
        map.setExpirationPolicy("1", ExpirationPolicy.ACCESSED);

        //动态添加和删除过期侦听器:
        ExpirationListener<String, String> connectionCloser = (key, value) -> System.out.println(key+":"+value);
        //添加侦听器
        map.addExpirationListener(connectionCloser);
        //移除侦听器
        map.removeExpirationListener(connectionCloser);

        //设置懒加载
//        Map<String, String> stringMap = ExpiringMap.builder()
//                .expiration(10, TimeUnit.MINUTES)
//                .entryLoader(address -> address)
//                .build();
//        // 通过 EntryLoader 将值加载到map中
//        String value = stringMap.get("1");
//        System.out.println("value值:"+value);

        //获取条目的到期时间:单位:毫秒
        long expiration = map.getExpectedExpiration("1");
        System.out.println("距离过期时间还有:"+expiration+"毫秒");

        //重置条目的内部到期计时器:
        map.resetExpiration("1");

        //查看设置的过期时间
        map.getExpiration("1");
        System.out.println("设置的过期时间:"+map.getExpiration("1"));

3.2 Google的Guava的LoadingCache

引入依赖

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>24.1-jre</version>
</dependency>
maximumSize:缓存的k-v最大数据,当总缓存的数据量达到这个值时,就会淘汰它认为不太用的一份数据,会使用LRU策略进行回收;
expireAfterAccess:缓存项在给定时间内没有被读/写访问,则回收,这个策略主要是为了淘汰长时间不被访问的数据;
expireAfterWrite:缓存项在给定时间内没有被写访问(创建或覆盖),则回收, 防止旧数据被缓存过久;
refreshAfterWrite:缓存项在给定时间内没有被写访问(创建或覆盖),则刷新;
recordStats:开启Cache的状态统计(默认是开启的);
removalListener:移除监听器,缓存项被移除时会触发
build:处理缓存键对应的缓存值不存在时的处理逻辑
   
 public static LoadingCache<Long, String> userCache= CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterAccess(60, TimeUnit.SECONDS)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .refreshAfterWrite(10, TimeUnit.SECONDS)
                .removalListener(new RemovalListener() {
                    @Override
                    public void onRemoval(RemovalNotification rn) {
                        log.error(rn.getKey() + "remove");
                    }
                })
                .build(new CacheLoader<Long, String>() {

                    @Override
                    public String load(Long aLong) throws Exception {
                        return "";
                    }
                });


网站公告

今日签到

点亮在社区的每一天
去签到