package edu.whut.smilepicturebackend.manager.cache; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collection; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @Service @RequiredArgsConstructor public class MyCacheManager { private final ObjectMapper mapper; /** * 本地缓存 */ private final Cache localCache = Caffeine.newBuilder() .initialCapacity(1024) .maximumSize(10_000L) // 最大 10000 条 .expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除 .build(); /** * 分布式缓存 */ private final StringRedisTemplate stringRedisTemplate; public T getFromCacheOrDatabase( String cacheKey, TypeReference typeRef, Supplier dbSupplier, int redisExpireSeconds) { // 1) 本地缓存 String cachedValue = localCache.getIfPresent(cacheKey); if (cachedValue != null) { try { return mapper.readValue(cachedValue, typeRef); } catch (Exception ignore) {} } // 2) Redis ValueOperations ops = stringRedisTemplate.opsForValue(); cachedValue = ops.get(cacheKey); if (cachedValue != null) { localCache.put(cacheKey, cachedValue); try { return mapper.readValue(cachedValue, typeRef); } catch (Exception ignore) {} } // 3) DB 回源 T dbValue = dbSupplier.get(); // ===== 空值缓存防穿透 ===== if (isEmptyValue(dbValue)) { String emptyJson = "{}"; ops.set(cacheKey, emptyJson, 60, TimeUnit.SECONDS); // 缓存60秒 localCache.put(cacheKey, emptyJson); return dbValue; } try { String json = mapper.writeValueAsString(dbValue); ops.set(cacheKey, json, redisExpireSeconds, TimeUnit.SECONDS); localCache.put(cacheKey, json); } catch (Exception ignore) {} return dbValue; } /** * 判断值是否为空,用于空值缓存 */ private boolean isEmptyValue(Object value) { if (value == null) { return true; } if (value instanceof Collection) { return ((Collection) value).isEmpty(); } if (value instanceof Map) { return ((Map) value).isEmpty(); } // 这里可以根据你的项目补充 Page 判空逻辑 if (value instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { return ((com.baomidou.mybatisplus.extension.plugins.pagination.Page) value).getRecords().isEmpty(); } return false; } /** * 删除缓存 * @param spaceId */ public void clearCacheBySpaceId(Long spaceId) { // 构造命名空间,公共图库使用 "public",其他空间使用 spaceId String namespace = (spaceId == null) ? "public" : String.valueOf(spaceId); // Redis SCAN 模式匹配的前缀 String pattern = "smilepicture:listPictureVOByPage:spaceId:" + namespace + ":*"; // 使用 SCAN 命令进行遍历,查找所有相关的缓存 key ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build(); Cursor cursor = stringRedisTemplate.execute( (redisConnection) -> redisConnection.scan(options), true ); // 删除匹配的缓存 while (cursor.hasNext()) { byte[] rawKey = cursor.next(); String cacheKey = new String(rawKey, StandardCharsets.UTF_8); // 转换 byte[] 为 String stringRedisTemplate.delete(cacheKey); // 删除 Redis 缓存 } // 删除本地缓存 Caffeine // 由于 Caffeine 不支持像 Redis 那样通过模式匹配查询所有 key,我们依然按分页模式来清除 for (int current = 1; current <= 100; current++) { // 假设最多查询100页 for (int size = 10; size <= 100; size += 10) { String cacheKey = "smilepicture:listPictureVOByPage:spaceId:" + namespace + ":current:" + current + ":size:" + size; localCache.invalidate(cacheKey); // 清除本地缓存 } } } }