package com.knowledge.base.infrastructure.cache; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; /** * 使用 Redis 实现的文件缓存服务 * * @author Luke.ye * @date 2025/6/7 */ @Slf4j @Service @RequiredArgsConstructor @ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "true", matchIfMissing = true) public class RedisFileCacheService implements FileCacheService { private final StringRedisTemplate redisTemplate; private static final String FILE_CACHE_PREFIX = "kb-file"; private String key(String prefix, String key) { return String.format(FILE_CACHE_PREFIX + ":%s:%s", prefix, key); } @Override public Optional getPath(String filename) { String value = redisTemplate.opsForValue().get(key("path", filename)); return Optional.ofNullable(value); } @Override public void cachePath(String filename, String relativePath, long expireMinutes) { redisTemplate.opsForValue().set(key("path", filename), relativePath, expireMinutes, TimeUnit.MINUTES); } @Override public void cacheMeta(String filename, String jsonMeta, long expireMinutes) { redisTemplate.opsForValue().set(key("meta", filename), jsonMeta, expireMinutes, TimeUnit.MINUTES); } @Override public Optional getMeta(String filename) { String value = redisTemplate.opsForValue().get(key("meta", filename)); return Optional.ofNullable(value); } @Override public Optional getMTime(String relativePath) { String val = redisTemplate.opsForValue().get(key("mtime", relativePath)); try { return val != null ? Optional.of(Long.parseLong(val)) : Optional.empty(); } catch (NumberFormatException e) { log.warn("mtime 解析失败: {}", val, e); return Optional.empty(); } } @Override public void cacheMTime(String relativePath, Long mtime, long expireMinutes) { redisTemplate.opsForValue().set(key("mtime", relativePath), String.valueOf(mtime), expireMinutes, TimeUnit.MINUTES); } @Override public void clearPath(String filename) { redisTemplate.delete(key("path", filename)); } @Override public void clearMTime(String relativePath) { redisTemplate.delete(key("mtime", relativePath)); } @Override public void clearAll() { log.warn("正在清空 Redis 文件缓存,前缀: {}", FILE_CACHE_PREFIX); Set keysToDelete = scanKeys(FILE_CACHE_PREFIX + "*"); if (keysToDelete.isEmpty()) { log.info("没有需要清理的文件缓存 Key。"); return; } redisTemplate.delete(keysToDelete); log.info("已清空 Redis 文件缓存 Key 数量: {}", keysToDelete.size()); } /** * 用 scan 命令遍历所有带 prefix 的 key(推荐,不会阻塞大 Redis) */ private Set scanKeys(String pattern) { Set keySet = new HashSet<>(); // 采用 scan,分批遍历,安全高效 redisTemplate.execute((connection) -> { try (var cursor = connection.scan(org.springframework.data.redis.core.ScanOptions.scanOptions() .match(pattern) .count(1000) .build())) { cursor.forEachRemaining(item -> keySet.add(new String(item))); } return null; }, false, false); return keySet; } }