8.11 缓存优化,添加增删改时缓存失效逻辑
This commit is contained in:
parent
97e4fe2763
commit
34909150be
@ -1,21 +1,27 @@
|
|||||||
package edu.whut.smilepicturebackend.manager.cache;
|
package edu.whut.smilepicturebackend.manager.cache;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.StringRedisTemplate;
|
||||||
import org.springframework.data.redis.core.ValueOperations;
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MyCacheManager {
|
public class MyCacheManager {
|
||||||
|
private final ObjectMapper mapper;
|
||||||
/**
|
/**
|
||||||
* 本地缓存
|
* 本地缓存
|
||||||
*/
|
*/
|
||||||
@ -30,34 +36,101 @@ public class MyCacheManager {
|
|||||||
*/
|
*/
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
// ====== 新增:强类型版本 ======
|
||||||
public <T> T getFromCacheOrDatabase(
|
public <T> T getFromCacheOrDatabase(
|
||||||
String cacheKey,
|
String cacheKey,
|
||||||
Class<T> clazz,
|
TypeReference<T> typeRef,
|
||||||
Supplier<T> dbSupplier,
|
Supplier<T> dbSupplier,
|
||||||
int redisExpireSeconds) {
|
int redisExpireSeconds) {
|
||||||
|
|
||||||
// 查询本地缓存
|
// 1) 本地缓存
|
||||||
String cachedValue = localCache.getIfPresent(cacheKey);
|
String cachedValue = localCache.getIfPresent(cacheKey);
|
||||||
if (cachedValue != null) {
|
if (cachedValue != null) {
|
||||||
return JSONUtil.toBean(cachedValue, clazz);
|
try {
|
||||||
|
return mapper.readValue(cachedValue, typeRef);
|
||||||
|
} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询 Redis 缓存
|
// 2) Redis
|
||||||
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
|
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
|
||||||
cachedValue = opsForValue.get(cacheKey);
|
cachedValue = ops.get(cacheKey);
|
||||||
if (cachedValue != null) {
|
if (cachedValue != null) {
|
||||||
localCache.put(cacheKey, cachedValue);
|
localCache.put(cacheKey, cachedValue);
|
||||||
return JSONUtil.toBean(cachedValue, clazz);
|
try {
|
||||||
|
return mapper.readValue(cachedValue, typeRef);
|
||||||
|
} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行数据库查询
|
// 3) DB 回源
|
||||||
T dbValue = dbSupplier.get();
|
T dbValue = dbSupplier.get();
|
||||||
|
|
||||||
// 更新 Redis 缓存
|
// ===== 空值缓存防穿透 =====
|
||||||
cachedValue = JSONUtil.toJsonStr(dbValue);
|
if (isEmptyValue(dbValue)) {
|
||||||
opsForValue.set(cacheKey, cachedValue, redisExpireSeconds, TimeUnit.SECONDS);
|
String emptyJson = "{}";
|
||||||
// 写入本地缓存
|
ops.set(cacheKey, emptyJson, 60, TimeUnit.SECONDS); // 缓存60秒
|
||||||
localCache.put(cacheKey, cachedValue);
|
localCache.put(cacheKey, emptyJson);
|
||||||
return dbValue;
|
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); // 清除本地缓存
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import javax.annotation.PreDestroy;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PictureEditEventProducer {
|
public class PictureEditEventProducer {
|
||||||
|
|
||||||
|
//注入无锁环形队列
|
||||||
private final Disruptor<PictureEditEvent> pictureEditEventDisruptor;
|
private final Disruptor<PictureEditEvent> pictureEditEventDisruptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,6 +23,8 @@ import edu.whut.smilepicturebackend.exception.ErrorCode;
|
|||||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||||
import edu.whut.smilepicturebackend.manager.CosManager;
|
import edu.whut.smilepicturebackend.manager.CosManager;
|
||||||
import edu.whut.smilepicturebackend.manager.FileManager;
|
import edu.whut.smilepicturebackend.manager.FileManager;
|
||||||
|
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||||
|
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||||
import edu.whut.smilepicturebackend.manager.cache.MyCacheManager;
|
import edu.whut.smilepicturebackend.manager.cache.MyCacheManager;
|
||||||
import edu.whut.smilepicturebackend.manager.upload.FilePictureUpload;
|
import edu.whut.smilepicturebackend.manager.upload.FilePictureUpload;
|
||||||
import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate;
|
import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate;
|
||||||
@ -48,6 +50,9 @@ import org.jsoup.nodes.Document;
|
|||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
@ -93,6 +98,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
|
|
||||||
private final AliYunAiApi aliYunAiApi;
|
private final AliYunAiApi aliYunAiApi;
|
||||||
|
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validPicture(Picture picture) {
|
public void validPicture(Picture picture) {
|
||||||
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
|
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
|
||||||
@ -243,6 +250,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
|
|
||||||
return picture; // 事务块返回结果
|
return picture; // 事务块返回结果
|
||||||
});
|
});
|
||||||
|
cacheManager.clearCacheBySpaceId(spaceId);
|
||||||
//如果是更新,清理旧的图片
|
//如果是更新,清理旧的图片
|
||||||
if (oldPicture != null) {
|
if (oldPicture != null) {
|
||||||
this.clearPictureFile(oldPicture);
|
this.clearPictureFile(oldPicture);
|
||||||
@ -330,7 +338,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}));
|
}));
|
||||||
|
cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId());
|
||||||
// 事务成功后再清理物理文件
|
// 事务成功后再清理物理文件
|
||||||
if (Boolean.TRUE.equals(txSuccess)) {
|
if (Boolean.TRUE.equals(txSuccess)) {
|
||||||
this.clearPictureFile(oldPicture);
|
this.clearPictureFile(oldPicture);
|
||||||
@ -360,6 +368,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
// 操作数据库
|
// 操作数据库
|
||||||
boolean result = this.updateById(picture);
|
boolean result = this.updateById(picture);
|
||||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||||
|
cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -437,6 +446,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
updatePicture.setReviewTime(new Date());
|
updatePicture.setReviewTime(new Date());
|
||||||
boolean result = this.updateById(updatePicture);
|
boolean result = this.updateById(updatePicture);
|
||||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||||
|
cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -457,6 +467,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
// 非管理员,无论是编辑还是创建默认都是待审核
|
// 非管理员,无论是编辑还是创建默认都是待审核
|
||||||
picture.setReviewStatus(PictureReviewStatusEnum.REVIEWING.getValue());
|
picture.setReviewStatus(PictureReviewStatusEnum.REVIEWING.getValue());
|
||||||
}
|
}
|
||||||
|
cacheManager.clearCacheBySpaceId(picture.getSpaceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
//爬取网落图片,可以用ai分析标签
|
//爬取网落图片,可以用ai分析标签
|
||||||
@ -567,7 +578,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cacheManager.clearCacheBySpaceId(null);
|
||||||
return uploadCount;
|
return uploadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,31 +603,43 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
long current = queryRequest.getCurrent();
|
long current = queryRequest.getCurrent();
|
||||||
long size = queryRequest.getPageSize();
|
long size = queryRequest.getPageSize();
|
||||||
|
|
||||||
// 参数校验
|
|
||||||
|
// 限制爬虫
|
||||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||||
|
// ====== 补齐“空间与权限”逻辑(和无缓存版保持一致)======
|
||||||
|
Long spaceId = queryRequest.getSpaceId();
|
||||||
|
if (spaceId == null) {
|
||||||
|
// 公开图库:仅看审核通过
|
||||||
queryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
queryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||||
|
queryRequest.setNullSpaceId(true);
|
||||||
|
} else {
|
||||||
|
// 私有/团队空间:校验空间查看权限
|
||||||
|
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||||
|
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
// 构造 cacheKey
|
|
||||||
String condJson = JSONUtil.toJsonStr(queryRequest);
|
|
||||||
String hash = DigestUtils.md5DigestAsHex(condJson.getBytes());
|
|
||||||
String cacheKey = "smilepicture:listPictureVOByPage:" + hash;
|
|
||||||
|
|
||||||
|
String cacheKey = "smilepicture:listPictureVOByPage:spaceId:" + (spaceId == null ? "public" : spaceId) +
|
||||||
|
":current:" + current +
|
||||||
|
":size:" + size;
|
||||||
|
|
||||||
|
// String cacheKey = "smilepicture:list:v" + ver + ":" + hash;
|
||||||
// 随机过期:300–600s
|
// 随机过期:300–600s
|
||||||
int expire = 300 + RandomUtil.randomInt(0, 300);
|
int expire = 300 + RandomUtil.randomInt(0, 300);
|
||||||
|
|
||||||
// 调用通用缓存方法
|
// 调用通用缓存方法
|
||||||
return cacheManager.getFromCacheOrDatabase(
|
return cacheManager.getFromCacheOrDatabase(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
Page.class,
|
new com.fasterxml.jackson.core.type.TypeReference<Page<PictureVO>>() {}, // 强类型
|
||||||
() -> {
|
() -> {
|
||||||
Page<Picture> picturePage = this.page(new Page<>(current, size),
|
Page<Picture> picturePage = this.page(new Page<>(current, size), this.getQueryWrapper(queryRequest));
|
||||||
this.getQueryWrapper(queryRequest));
|
|
||||||
return this.getPictureVOPage(picturePage, httpRequest);
|
return this.getPictureVOPage(picturePage, httpRequest);
|
||||||
},
|
},
|
||||||
expire
|
expire
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Async //异步执行
|
@Async //异步执行
|
||||||
@Override
|
@Override
|
||||||
public void clearPictureFile(Picture oldPicture) {
|
public void clearPictureFile(Picture oldPicture) {
|
||||||
@ -776,6 +799,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
// 5. 操作数据库进行批量更新
|
// 5. 操作数据库进行批量更新
|
||||||
boolean result = this.updateBatchById(pictureList);
|
boolean result = this.updateBatchById(pictureList);
|
||||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败");
|
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败");
|
||||||
|
cacheManager.clearCacheBySpaceId(spaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user