diff --git a/pom.xml b/pom.xml index 7468a95..14b2e0d 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,21 @@ jsoup 1.15.3 + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + + + org.springframework.boot + spring-boot-configuration-processor + com.mysql mysql-connector-j diff --git a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java index 714ec0f..34447ed 100644 --- a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java +++ b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java @@ -1,7 +1,10 @@ package edu.whut.smilepicturebackend.controller; +import cn.hutool.core.util.RandomUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import edu.whut.smilepicturebackend.annotation.AuthCheck; import edu.whut.smilepicturebackend.common.BaseResponse; import edu.whut.smilepicturebackend.common.DeleteRequest; @@ -21,12 +24,17 @@ import edu.whut.smilepicturebackend.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; +import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; @Slf4j @RestController @@ -35,6 +43,16 @@ import java.util.List; public class PictureController { private final UserService userService; private final PictureService pictureService; + private final StringRedisTemplate stringRedisTemplate; + + /** + * 本地缓存 + */ + private final Cache LOCAL_CACHE = Caffeine.newBuilder() + .initialCapacity(1024) + .maximumSize(10_000L) // 最大 10000 条 + .expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除 + .build(); /** * 上传图片(可重新上传) @@ -188,6 +206,52 @@ public class PictureController { return ResultUtils.success(pictureService.getPictureVOPage(picturePage, request)); } + /** + * 分页获取图片列表(封装类,有缓存) + */ + @Deprecated + @PostMapping("/list/page/vo/cache") + public BaseResponse> listPictureVOByPageWithCache(@RequestBody PictureQueryRequest pictureQueryRequest, + HttpServletRequest request) { + long current = pictureQueryRequest.getCurrent(); + long size = pictureQueryRequest.getPageSize(); + // 限制爬虫 + ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); + // 普通用户默认只能看到审核通过的数据 + pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue()); + // 查询缓存,缓存中没有,再查询数据库 + // 构建缓存的 key,根据查询条件来构建->json + String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest); //json->md5,更轻量化,去除括号,双引号 + String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes()); + String cacheKey = String.format("smilepicture:listPictureVOByPage:%s", hashKey); +// // 1. 先从本地缓存中查询 +// String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey); +// if (cachedValue != null) { +// // 如果缓存命中,返回结果 +// Page cachedPage = JSONUtil.toBean(cachedValue, Page.class); +// return ResultUtils.success(cachedPage); +// } + // 2. 本地缓存未命中,查询 Redis 分布式缓存 + ValueOperations opsForValue = stringRedisTemplate.opsForValue(); + String cachedValue = opsForValue.get(cacheKey); + if (cachedValue != null) { + Page cachedPage = JSONUtil.toBean(cachedValue, Page.class); + return ResultUtils.success(cachedPage); + } + // 3. 查询数据库 + Page picturePage = pictureService.page(new Page<>(current, size), + pictureService.getQueryWrapper(pictureQueryRequest)); + Page pictureVOPage = pictureService.getPictureVOPage(picturePage, request); + // 4. 更新缓存 + // 更新 Redis 缓存 + String cacheValue = JSONUtil.toJsonStr(pictureVOPage); + // 设置缓存的过期时间,5 - 10 分钟过期,防止缓存雪崩! + int cacheExpireTime = 300 + RandomUtil.randomInt(0, 300); + opsForValue.set(cacheKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS); + // 获取封装类 + return ResultUtils.success(pictureVOPage); + } + /** * 编辑图片(给用户使用) */ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 875a642..e12c7a3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,13 @@ spring: url: jdbc:mysql://localhost:3306/smile-picture username: root password: 123456 + # Redis 配置 + redis: + database: 1 + host: 127.0.0.1 + port: 6379 + password: 123456 + timeout: 5000 servlet: multipart: max-file-size: 10MB diff --git a/src/test/java/edu/whut/smilepicturebackend/RedisStringTest.java b/src/test/java/edu/whut/smilepicturebackend/RedisStringTest.java new file mode 100644 index 0000000..af1d11e --- /dev/null +++ b/src/test/java/edu/whut/smilepicturebackend/RedisStringTest.java @@ -0,0 +1,48 @@ +package edu.whut.smilepicturebackend; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class RedisStringTest { + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Test + public void testRedisStringOperations() { + // 获取操作对象 + ValueOperations valueOps = stringRedisTemplate.opsForValue(); + + // Key 和 Value + String key = "testKey"; + String value = "testValue"; + + // 1. 测试新增或更新操作 + valueOps.set(key, value); + String storedValue = valueOps.get(key); + assertEquals(value, storedValue, "存储的值与预期不一致"); + + // 2. 测试修改操作 + String updatedValue = "updatedValue"; + valueOps.set(key, updatedValue); + storedValue = valueOps.get(key); + assertEquals(updatedValue, storedValue, "更新后的值与预期不一致"); + + // 3. 测试查询操作 + storedValue = valueOps.get(key); + assertNotNull(storedValue, "查询的值为空"); + assertEquals(updatedValue, storedValue, "查询的值与预期不一致"); + +// // 4. 测试删除操作 +// redisTemplate.delete(key); +// storedValue = valueOps.get(key); +// assertNull(storedValue, "删除后的值不为空"); + } +} \ No newline at end of file