2025-03-14 16:55:32 +08:00
|
|
|
|
package edu.whut.smilepicturebackend.manager.cache;
|
2025-08-11 11:54:36 +08:00
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
import com.github.benmanes.caffeine.cache.Cache;
|
|
|
|
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
2025-08-11 11:54:36 +08:00
|
|
|
|
import org.springframework.data.redis.core.Cursor;
|
|
|
|
|
import org.springframework.data.redis.core.ScanOptions;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
|
|
import org.springframework.data.redis.core.ValueOperations;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
2025-08-11 11:54:36 +08:00
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
import java.nio.charset.StandardCharsets;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
import java.time.Duration;
|
2025-08-11 11:54:36 +08:00
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.Map;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
|
2025-03-14 16:55:32 +08:00
|
|
|
|
@Service
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
public class MyCacheManager {
|
2025-08-11 11:54:36 +08:00
|
|
|
|
private final ObjectMapper mapper;
|
2025-03-14 16:55:32 +08:00
|
|
|
|
/**
|
|
|
|
|
* 本地缓存
|
|
|
|
|
*/
|
|
|
|
|
private final Cache<String, String> localCache = Caffeine.newBuilder()
|
|
|
|
|
.initialCapacity(1024)
|
|
|
|
|
.maximumSize(10_000L) // 最大 10000 条
|
|
|
|
|
.expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 分布式缓存
|
|
|
|
|
*/
|
|
|
|
|
private final StringRedisTemplate stringRedisTemplate;
|
|
|
|
|
|
|
|
|
|
public <T> T getFromCacheOrDatabase(
|
|
|
|
|
String cacheKey,
|
2025-08-11 11:54:36 +08:00
|
|
|
|
TypeReference<T> typeRef,
|
2025-03-14 16:55:32 +08:00
|
|
|
|
Supplier<T> dbSupplier,
|
|
|
|
|
int redisExpireSeconds) {
|
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
// 1) 本地缓存
|
2025-03-14 16:55:32 +08:00
|
|
|
|
String cachedValue = localCache.getIfPresent(cacheKey);
|
|
|
|
|
if (cachedValue != null) {
|
2025-08-11 11:54:36 +08:00
|
|
|
|
try {
|
|
|
|
|
return mapper.readValue(cachedValue, typeRef);
|
|
|
|
|
} catch (Exception ignore) {}
|
2025-03-14 16:55:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
// 2) Redis
|
|
|
|
|
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
|
|
|
|
|
cachedValue = ops.get(cacheKey);
|
2025-03-14 16:55:32 +08:00
|
|
|
|
if (cachedValue != null) {
|
|
|
|
|
localCache.put(cacheKey, cachedValue);
|
2025-08-11 11:54:36 +08:00
|
|
|
|
try {
|
|
|
|
|
return mapper.readValue(cachedValue, typeRef);
|
|
|
|
|
} catch (Exception ignore) {}
|
2025-03-14 16:55:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
// 3) DB 回源
|
2025-03-14 16:55:32 +08:00
|
|
|
|
T dbValue = dbSupplier.get();
|
|
|
|
|
|
2025-08-11 11:54:36 +08:00
|
|
|
|
// ===== 空值缓存防穿透 =====
|
|
|
|
|
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) {}
|
2025-03-14 16:55:32 +08:00
|
|
|
|
return dbValue;
|
|
|
|
|
}
|
2025-08-11 11:54:36 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断值是否为空,用于空值缓存
|
|
|
|
|
*/
|
|
|
|
|
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<byte[]> 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); // 清除本地缓存
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-14 16:55:32 +08:00
|
|
|
|
}
|