我们在项目中会经常使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() {@Overridepublic void onRemoval(RemovalNotification rn) {log.error(rn.getKey() + "remove");}}).build(new CacheLoader<Long, String>() {@Overridepublic String load(Long aLong) throws Exception {return "";}});