136 lines
4.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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,
TypeReference<T> typeRef,
Supplier<T> dbSupplier,
int redisExpireSeconds) {
// 1) 本地缓存
String cachedValue = localCache.getIfPresent(cacheKey);
if (cachedValue != null) {
try {
return mapper.readValue(cachedValue, typeRef);
} catch (Exception ignore) {}
}
// 2) Redis
ValueOperations<String, String> 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<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); // 清除本地缓存
}
}
}
}