diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java index be753f2..8a7171d 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java @@ -13,6 +13,7 @@ import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.exceptions.StorageUnsupportedException; import com.ycwl.basic.util.ScenicConfigManager; +import com.ycwl.basic.util.TtlCacheMap; import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.JacksonUtil; import lombok.extern.slf4j.Slf4j; @@ -21,7 +22,7 @@ import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * @Author:longbinbin @@ -32,6 +33,21 @@ import java.util.concurrent.ConcurrentHashMap; public class ScenicServiceImpl implements ScenicService { @Autowired private ScenicRepository scenicRepository; + + // TTL缓存配置,默认10分钟过期 + private static final long DEFAULT_CACHE_TTL_MINUTES = 10; + + // 使用TTL缓存替代静态Map + private static final TtlCacheMap scenicStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicTmpStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicLocalStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicFaceBodyAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicPayAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); @Override @Deprecated @@ -39,10 +55,9 @@ public class ScenicServiceImpl implements ScenicService { return ApiResponse.success(scenicRepository.list(scenicReqQuery)); } - private static final Map scenicStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicStorageAdapter(Long scenicId) { - return scenicStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); if (scenicConfig.getString("store_type") != null) { @@ -58,10 +73,9 @@ public class ScenicServiceImpl implements ScenicService { return adapter; }); } - private static final Map scenicTmpStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicTmpStorageAdapter(Long scenicId) { - return scenicTmpStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicTmpStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); if (scenicConfig.getString("tmp_store_type") != null) { @@ -77,10 +91,9 @@ public class ScenicServiceImpl implements ScenicService { return adapter; }); } - private static final Map scenicLocalStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicLocalStorageAdapter(Long scenicId) { - return scenicLocalStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicLocalStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); if (scenicConfig.getString("local_store_type") != null) { @@ -97,10 +110,9 @@ public class ScenicServiceImpl implements ScenicService { }); } - private static final Map scenicFaceBodyAdapterMap = new ConcurrentHashMap<>(); @Override public IFaceBodyAdapter getScenicFaceBodyAdapter(Long scenicId) { - return scenicFaceBodyAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicFaceBodyAdapterCache.computeIfAbsent(scenicId, (key) -> { IFaceBodyAdapter adapter; ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); if (scenicConfig.getString("face_type") != null) { @@ -113,10 +125,9 @@ public class ScenicServiceImpl implements ScenicService { }); } - private static final Map scenicPayAdapterMap = new ConcurrentHashMap<>(); @Override public IPayAdapter getScenicPayAdapter(Long scenicId) { - return scenicPayAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicPayAdapterCache.computeIfAbsent(scenicId, (key) -> { IPayAdapter adapter; ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); if (scenicConfig.getString("pay_type") != null) { @@ -128,4 +139,94 @@ public class ScenicServiceImpl implements ScenicService { return adapter; }); } + + // ==================== 缓存管理方法 ==================== + + /** + * 清除指定景区的所有适配器缓存 + * + * @param scenicId 景区ID + */ + public void clearScenicAdapterCache(Long scenicId) { + log.info("清除景区 {} 的所有适配器缓存", scenicId); + scenicStorageAdapterCache.remove(scenicId); + scenicTmpStorageAdapterCache.remove(scenicId); + scenicLocalStorageAdapterCache.remove(scenicId); + scenicFaceBodyAdapterCache.remove(scenicId); + scenicPayAdapterCache.remove(scenicId); + } + + /** + * 清除所有适配器缓存 + */ + public void clearAllAdapterCache() { + log.info("清除所有适配器缓存"); + scenicStorageAdapterCache.clear(); + scenicTmpStorageAdapterCache.clear(); + scenicLocalStorageAdapterCache.clear(); + scenicFaceBodyAdapterCache.clear(); + scenicPayAdapterCache.clear(); + } + + /** + * 手动触发过期缓存清理 + * + * @return 清理的过期缓存项总数 + */ + public int cleanupExpiredCache() { + log.info("手动触发过期缓存清理"); + int totalCleaned = 0; + totalCleaned += scenicStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicTmpStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicLocalStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicFaceBodyAdapterCache.cleanupExpired(); + totalCleaned += scenicPayAdapterCache.cleanupExpired(); + log.info("清理了 {} 个过期缓存项", totalCleaned); + return totalCleaned; + } + + /** + * 获取缓存统计信息 + * + * @return 缓存统计信息 + */ + public String getCacheStats() { + StringBuilder stats = new StringBuilder(); + stats.append("=== ScenicServiceImpl 缓存统计信息 ===\n"); + stats.append("Storage Adapter Cache: ").append(scenicStorageAdapterCache.getStats()).append("\n"); + stats.append("Tmp Storage Adapter Cache: ").append(scenicTmpStorageAdapterCache.getStats()).append("\n"); + stats.append("Local Storage Adapter Cache: ").append(scenicLocalStorageAdapterCache.getStats()).append("\n"); + stats.append("FaceBody Adapter Cache: ").append(scenicFaceBodyAdapterCache.getStats()).append("\n"); + stats.append("Pay Adapter Cache: ").append(scenicPayAdapterCache.getStats()).append("\n"); + return stats.toString(); + } + + /** + * 重置所有缓存统计信息 + */ + public void resetCacheStats() { + log.info("重置所有缓存统计信息"); + scenicStorageAdapterCache.resetStats(); + scenicTmpStorageAdapterCache.resetStats(); + scenicLocalStorageAdapterCache.resetStats(); + scenicFaceBodyAdapterCache.resetStats(); + scenicPayAdapterCache.resetStats(); + } + + /** + * 获取指定景区缓存的剩余TTL时间 + * + * @param scenicId 景区ID + * @return 各类型适配器缓存的剩余TTL时间(毫秒) + */ + public String getScenicCacheTtl(Long scenicId) { + StringBuilder ttlInfo = new StringBuilder(); + ttlInfo.append("景区 ").append(scenicId).append(" 缓存TTL信息:\n"); + ttlInfo.append("Storage: ").append(scenicStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("TmpStorage: ").append(scenicTmpStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("LocalStorage: ").append(scenicLocalStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("FaceBody: ").append(scenicFaceBodyAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("Pay: ").append(scenicPayAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + return ttlInfo.toString(); + } } diff --git a/src/main/java/com/ycwl/basic/util/TtlCacheMap.java b/src/main/java/com/ycwl/basic/util/TtlCacheMap.java new file mode 100644 index 0000000..fc5f91f --- /dev/null +++ b/src/main/java/com/ycwl/basic/util/TtlCacheMap.java @@ -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 键类型 + * @param 值类型 + */ +@Slf4j +public class TtlCacheMap { + + /** + * 缓存项包装类 + */ + private static class CacheItem { + 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> 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 valueSupplier) { + return computeIfAbsent(key, valueSupplier, defaultTtlMillis); + } + + /** + * 获取缓存值,如果不存在或过期则通过supplier创建 + * + * @param key 缓存键 + * @param valueSupplier 值提供器 + * @param ttlMillis TTL时间(毫秒) + * @return 缓存值 + */ + public V computeIfAbsent(K key, Function valueSupplier, long ttlMillis) { + lock.readLock().lock(); + try { + CacheItem item = cache.get(key); + if (item != null && !item.isExpired()) { + hitCount++; + return item.getValue(); + } + } finally { + lock.readLock().unlock(); + } + + // 缓存不存在或已过期,需要重新创建 + lock.writeLock().lock(); + try { + // 双重检查,防止重复创建 + CacheItem 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 item = cache.get(key); + if (item != null) { + if (!item.isExpired()) { + hitCount++; + return item.getValue(); + } else { + // 异步清理过期项 + cleanupExecutor.execute(() -> { + lock.writeLock().lock(); + try { + CacheItem 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 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 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(); + } + } +} \ No newline at end of file