我们在项目中会经常使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 "";
}
});