feat(service): 使用带TTL的缓存Map替换静态Map

- 新增TtlCacheMap类,用于实现带生存时间的缓存
- 在ScenicServiceImpl中使用TtlCacheMap替换原有的ConcurrentHashMap
- 为不同类型的适配器创建了对应的缓存Map
- 优化了缓存获取逻辑,增加了TTL支持
- 添加了缓存清理和统计功能
This commit is contained in:
2025-08-28 16:02:30 +08:00
parent 46fb255e66
commit 798ff3b9b5
2 changed files with 452 additions and 11 deletions

View File

@@ -0,0 +1,340 @@
package com.ycwl.basic.util;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
/**
* 带TTL(生存时间)的缓存Map工具类
*
* @param <K> 键类型
* @param <V> 值类型
*/
@Slf4j
public class TtlCacheMap<K, V> {
/**
* 缓存项包装类
*/
private static class CacheItem<V> {
private final V value;
private final long expireTime;
public CacheItem(V value, long ttlMillis) {
this.value = value;
this.expireTime = System.currentTimeMillis() + ttlMillis;
}
public V getValue() {
return value;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
public long getRemainTtl() {
return Math.max(0, expireTime - System.currentTimeMillis());
}
}
private final ConcurrentHashMap<K, CacheItem<V>> cache;
private final long defaultTtlMillis;
private final ReentrantReadWriteLock lock;
private final ScheduledExecutorService cleanupExecutor;
// 统计信息
private volatile long hitCount = 0;
private volatile long missCount = 0;
private volatile long expiredCount = 0;
/**
* 构造函数
*
* @param defaultTtlMillis 默认TTL时间(毫秒)
*/
public TtlCacheMap(long defaultTtlMillis) {
this.cache = new ConcurrentHashMap<>();
this.defaultTtlMillis = defaultTtlMillis;
this.lock = new ReentrantReadWriteLock();
this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "TtlCacheMap-Cleanup");
t.setDaemon(true);
return t;
});
// 启动定期清理任务,每分钟清理一次过期条目
this.cleanupExecutor.scheduleWithFixedDelay(this::cleanupExpired, 60, 60, TimeUnit.SECONDS);
}
/**
* 构造函数,使用默认TTL为10分钟
*/
public TtlCacheMap() {
this(TimeUnit.MINUTES.toMillis(10));
}
/**
* 获取缓存值,如果不存在或过期则通过supplier创建
*
* @param key 缓存键
* @param valueSupplier 值提供器
* @return 缓存值
*/
public V computeIfAbsent(K key, Function<K, V> valueSupplier) {
return computeIfAbsent(key, valueSupplier, defaultTtlMillis);
}
/**
* 获取缓存值,如果不存在或过期则通过supplier创建
*
* @param key 缓存键
* @param valueSupplier 值提供器
* @param ttlMillis TTL时间(毫秒)
* @return 缓存值
*/
public V computeIfAbsent(K key, Function<K, V> valueSupplier, long ttlMillis) {
lock.readLock().lock();
try {
CacheItem<V> item = cache.get(key);
if (item != null && !item.isExpired()) {
hitCount++;
return item.getValue();
}
} finally {
lock.readLock().unlock();
}
// 缓存不存在或已过期,需要重新创建
lock.writeLock().lock();
try {
// 双重检查,防止重复创建
CacheItem<V> item = cache.get(key);
if (item != null && !item.isExpired()) {
hitCount++;
return item.getValue();
}
if (item != null && item.isExpired()) {
expiredCount++;
cache.remove(key);
}
// 创建新值
missCount++;
V value = valueSupplier.apply(key);
if (value != null) {
cache.put(key, new CacheItem<>(value, ttlMillis));
}
return value;
} finally {
lock.writeLock().unlock();
}
}
/**
* 直接放入缓存
*
* @param key 缓存键
* @param value 缓存值
*/
public void put(K key, V value) {
put(key, value, defaultTtlMillis);
}
/**
* 直接放入缓存
*
* @param key 缓存键
* @param value 缓存值
* @param ttlMillis TTL时间(毫秒)
*/
public void put(K key, V value, long ttlMillis) {
lock.writeLock().lock();
try {
cache.put(key, new CacheItem<>(value, ttlMillis));
} finally {
lock.writeLock().unlock();
}
}
/**
* 获取缓存值
*
* @param key 缓存键
* @return 缓存值,如果不存在或过期返回null
*/
public V get(K key) {
lock.readLock().lock();
try {
CacheItem<V> item = cache.get(key);
if (item != null) {
if (!item.isExpired()) {
hitCount++;
return item.getValue();
} else {
// 异步清理过期项
cleanupExecutor.execute(() -> {
lock.writeLock().lock();
try {
CacheItem<V> expiredItem = cache.get(key);
if (expiredItem != null && expiredItem.isExpired()) {
cache.remove(key);
expiredCount++;
}
} finally {
lock.writeLock().unlock();
}
});
}
}
missCount++;
return null;
} finally {
lock.readLock().unlock();
}
}
/**
* 移除缓存项
*
* @param key 缓存键
* @return 被移除的值,如果不存在返回null
*/
public V remove(K key) {
lock.writeLock().lock();
try {
CacheItem<V> item = cache.remove(key);
return item != null ? item.getValue() : null;
} finally {
lock.writeLock().unlock();
}
}
/**
* 清空所有缓存
*/
public void clear() {
lock.writeLock().lock();
try {
cache.clear();
} finally {
lock.writeLock().unlock();
}
}
/**
* 检查缓存键是否存在且未过期
*
* @param key 缓存键
* @return true如果存在且未过期
*/
public boolean containsKey(K key) {
return get(key) != null;
}
/**
* 获取缓存大小(包含过期项)
*
* @return 缓存大小
*/
public int size() {
return cache.size();
}
/**
* 检查缓存是否为空
*
* @return true如果为空
*/
public boolean isEmpty() {
return cache.isEmpty();
}
/**
* 手动触发过期清理
*
* @return 清理的过期项数量
*/
public int cleanupExpired() {
lock.writeLock().lock();
try {
int cleanedCount = 0;
var iterator = cache.entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
if (entry.getValue().isExpired()) {
iterator.remove();
cleanedCount++;
expiredCount++;
}
}
if (cleanedCount > 0) {
log.debug("清理了 {} 个过期缓存项", cleanedCount);
}
return cleanedCount;
} finally {
lock.writeLock().unlock();
}
}
/**
* 获取缓存统计信息
*
* @return 统计信息字符串
*/
public String getStats() {
long total = hitCount + missCount;
double hitRate = total > 0 ? (double) hitCount / total * 100 : 0;
return String.format(
"TtlCacheMap Stats: size=%d, hits=%d, misses=%d, expired=%d, hitRate=%.2f%%",
cache.size(), hitCount, missCount, expiredCount, hitRate
);
}
/**
* 重置统计信息
*/
public void resetStats() {
hitCount = 0;
missCount = 0;
expiredCount = 0;
}
/**
* 获取剩余TTL时间
*
* @param key 缓存键
* @return 剩余TTL毫秒数,如果不存在或已过期返回0
*/
public long getRemainTtl(K key) {
lock.readLock().lock();
try {
CacheItem<V> item = cache.get(key);
return item != null ? item.getRemainTtl() : 0;
} finally {
lock.readLock().unlock();
}
}
/**
* 关闭清理线程池,释放资源
*/
public void shutdown() {
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}