From 34909150bef1e24eedf0bc7c2e3b20c8beb2745c Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Mon, 11 Aug 2025 11:54:36 +0800 Subject: [PATCH] =?UTF-8?q?8.11=20=E7=BC=93=E5=AD=98=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=A2=9E=E5=88=A0=E6=94=B9=E6=97=B6?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E5=A4=B1=E6=95=88=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/cache/MyCacheManager.java | 105 +++++++++++++++--- .../disruptor/PictureEditEventProducer.java | 1 + .../service/impl/PictureServiceImpl.java | 46 ++++++-- 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java b/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java index 7558ca7..2ef37c1 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java @@ -1,21 +1,27 @@ package edu.whut.smilepicturebackend.manager.cache; - -import cn.hutool.json.JSONUtil; +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; /** * 本地缓存 */ @@ -30,34 +36,101 @@ public class MyCacheManager { */ private final StringRedisTemplate stringRedisTemplate; + // ====== 新增:强类型版本 ====== public T getFromCacheOrDatabase( String cacheKey, - Class clazz, + TypeReference typeRef, Supplier dbSupplier, int redisExpireSeconds) { - // 查询本地缓存 + // 1) 本地缓存 String cachedValue = localCache.getIfPresent(cacheKey); if (cachedValue != null) { - return JSONUtil.toBean(cachedValue, clazz); + try { + return mapper.readValue(cachedValue, typeRef); + } catch (Exception ignore) {} } - // 查询 Redis 缓存 - ValueOperations opsForValue = stringRedisTemplate.opsForValue(); - cachedValue = opsForValue.get(cacheKey); + // 2) Redis + ValueOperations ops = stringRedisTemplate.opsForValue(); + cachedValue = ops.get(cacheKey); if (cachedValue != null) { localCache.put(cacheKey, cachedValue); - return JSONUtil.toBean(cachedValue, clazz); + try { + return mapper.readValue(cachedValue, typeRef); + } catch (Exception ignore) {} } - // 执行数据库查询 + // 3) DB 回源 T dbValue = dbSupplier.get(); - // 更新 Redis 缓存 - cachedValue = JSONUtil.toJsonStr(dbValue); - opsForValue.set(cacheKey, cachedValue, redisExpireSeconds, TimeUnit.SECONDS); - // 写入本地缓存 - localCache.put(cacheKey, cachedValue); + // ===== 空值缓存防穿透 ===== + 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); // 清除本地缓存 + } + } + } } diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/websocket/disruptor/PictureEditEventProducer.java b/src/main/java/edu/whut/smilepicturebackend/manager/websocket/disruptor/PictureEditEventProducer.java index e9fe853..11d7283 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/websocket/disruptor/PictureEditEventProducer.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/websocket/disruptor/PictureEditEventProducer.java @@ -19,6 +19,7 @@ import javax.annotation.PreDestroy; @RequiredArgsConstructor public class PictureEditEventProducer { + //注入无锁环形队列 private final Disruptor pictureEditEventDisruptor; /** diff --git a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java index 925d9d6..0b927a0 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java @@ -23,6 +23,8 @@ import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ThrowUtils; import edu.whut.smilepicturebackend.manager.CosManager; 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.upload.FilePictureUpload; import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate; @@ -48,6 +50,9 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; 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.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; @@ -93,6 +98,8 @@ public class PictureServiceImpl extends ServiceImpl private final AliYunAiApi aliYunAiApi; + private final StringRedisTemplate stringRedisTemplate; + @Override public void validPicture(Picture picture) { ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR); @@ -243,6 +250,7 @@ public class PictureServiceImpl extends ServiceImpl return picture; // 事务块返回结果 }); + cacheManager.clearCacheBySpaceId(spaceId); //如果是更新,清理旧的图片 if (oldPicture != null) { this.clearPictureFile(oldPicture); @@ -330,7 +338,7 @@ public class PictureServiceImpl extends ServiceImpl } return true; })); - + cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId()); // 事务成功后再清理物理文件 if (Boolean.TRUE.equals(txSuccess)) { this.clearPictureFile(oldPicture); @@ -360,6 +368,7 @@ public class PictureServiceImpl extends ServiceImpl // 操作数据库 boolean result = this.updateById(picture); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); + cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId()); } @Override @@ -437,6 +446,7 @@ public class PictureServiceImpl extends ServiceImpl updatePicture.setReviewTime(new Date()); boolean result = this.updateById(updatePicture); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); + cacheManager.clearCacheBySpaceId(oldPicture.getSpaceId()); } /** @@ -457,6 +467,7 @@ public class PictureServiceImpl extends ServiceImpl // 非管理员,无论是编辑还是创建默认都是待审核 picture.setReviewStatus(PictureReviewStatusEnum.REVIEWING.getValue()); } + cacheManager.clearCacheBySpaceId(picture.getSpaceId()); } //爬取网落图片,可以用ai分析标签 @@ -567,7 +578,7 @@ public class PictureServiceImpl extends ServiceImpl break; } } - + cacheManager.clearCacheBySpaceId(null); return uploadCount; } @@ -592,31 +603,43 @@ public class PictureServiceImpl extends ServiceImpl long current = queryRequest.getCurrent(); long size = queryRequest.getPageSize(); - // 参数校验 + + // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); - queryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue()); + // ====== 补齐“空间与权限”逻辑(和无缓存版保持一致)====== + Long spaceId = queryRequest.getSpaceId(); + if (spaceId == null) { + // 公开图库:仅看审核通过 + 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 int expire = 300 + RandomUtil.randomInt(0, 300); // 调用通用缓存方法 return cacheManager.getFromCacheOrDatabase( cacheKey, - Page.class, + new com.fasterxml.jackson.core.type.TypeReference>() {}, // 强类型 () -> { - Page picturePage = this.page(new Page<>(current, size), - this.getQueryWrapper(queryRequest)); + Page picturePage = this.page(new Page<>(current, size), this.getQueryWrapper(queryRequest)); return this.getPictureVOPage(picturePage, httpRequest); }, expire ); } + @Async //异步执行 @Override public void clearPictureFile(Picture oldPicture) { @@ -776,6 +799,7 @@ public class PictureServiceImpl extends ServiceImpl // 5. 操作数据库进行批量更新 boolean result = this.updateBatchById(pictureList); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败"); + cacheManager.clearCacheBySpaceId(spaceId); } @Override