?我們在項目中會經常使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 "";}});