Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现首页数据缓存以及入参 limit 限制为最大50 #122

Merged
merged 10 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dependencies {
implementation 'com.github.erosb:everit-json-schema:1.14.2'
implementation 'com.google.guava:guava:32.1.1-jre'
implementation 'org.aspectj:aspectjweaver:1.9.19'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

swaggerCodegen 'org.openapitools:openapi-generator-cli:6.5.0'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,8 @@ public MaaResult<String> sendComments(
@Operation(summary = "分页查询评论")
@ApiResponse(description = "评论区信息")
public MaaResult<CommentsAreaInfo> queriesCommentsArea(
@RequestParam(name = "copilotId") Long copilotId,
@RequestParam(name = "page", required = false, defaultValue = "0") int page,
@RequestParam(name = "limit", required = false, defaultValue = "10") int limit,
@RequestParam(name = "desc", required = false, defaultValue = "true") boolean desc,
@RequestParam(name = "orderBy", required = false) String orderBy,
@RequestParam(name = "justSeeId", required = false) String justSeeId
@Parameter(description = "评论查询对象") @Valid CommentsQueriesDTO parsed
) {
var parsed = new CommentsQueriesDTO(
copilotId,
page,
limit,
desc,
orderBy,
justSeeId
);
return MaaResult.success(commentsAreaService.queriesCommentsArea(parsed));
}

Expand Down
26 changes: 3 additions & 23 deletions src/main/java/plus/maa/backend/controller/CopilotController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
Expand All @@ -14,9 +15,9 @@
import plus.maa.backend.controller.request.copilot.CopilotCUDRequest;
import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest;
import plus.maa.backend.controller.request.copilot.CopilotRatingReq;
import plus.maa.backend.controller.response.MaaResult;
import plus.maa.backend.controller.response.copilot.CopilotInfo;
import plus.maa.backend.controller.response.copilot.CopilotPageInfo;
import plus.maa.backend.controller.response.MaaResult;
import plus.maa.backend.service.CopilotService;

/**
Expand Down Expand Up @@ -70,29 +71,8 @@ public MaaResult<CopilotInfo> getCopilotById(
@ApiResponse(description = "作业信息")
@GetMapping("/query")
public MaaResult<CopilotPageInfo> queriesCopilot(
@RequestParam(name = "page", required = false, defaultValue = "0") int page,
@RequestParam(name = "limit", required = false, defaultValue = "10") int limit,
@RequestParam(name = "level_keyword", required = false) String levelKeyword,
@RequestParam(name = "operator", required = false) String operator,
@RequestParam(name = "content", required = false) String content,
@RequestParam(name = "document", required = false) String document,
@RequestParam(name = "uploader_id", required = false) String uploaderId,
@RequestParam(name = "desc", required = false, defaultValue = "true") boolean desc,
@RequestParam(name = "order_by", required = false) String orderBy,
@RequestParam(name = "language", required = false) String language
@Parameter(description = "作业查询请求") @Valid CopilotQueriesRequest parsed
) {
var parsed = new CopilotQueriesRequest(
page,
limit,
levelKeyword,
operator,
content,
document,
uploaderId,
desc,
orderBy,
language
);
return MaaResult.success(copilotService.queriesCopilot(helper.getUserId(), parsed));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package plus.maa.backend.controller.request.comments;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
Expand All @@ -14,10 +16,12 @@
@NoArgsConstructor
@AllArgsConstructor
public class CommentsQueriesDTO {
@NotNull(message = "作业id不可为空")
private Long copilotId;
private int page;
private int limit;
private boolean desc;
private int page = 0;
@Max(value = 50, message = "单页大小不得超过50")
private int limit = 10;
private boolean desc = true;
private String orderBy;
private String justSeeId;
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
package plus.maa.backend.controller.request.copilot;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Max;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author LoMu
* Date 2022-12-26 2:48
*/
@AllArgsConstructor
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CopilotQueriesRequest {
private int page;
private int limit;
private int page = 0;
@Max(value = 50, message = "单页大小不得超过50")
private int limit = 10;
@JsonAlias("level_keyword")
private String levelKeyword;
private String operator;
private String content;
private String document;
@JsonAlias("uploader_id")
private String uploaderId;
private boolean desc;
private boolean desc = true;
@JsonAlias("order_by")
private String orderBy;
private String language;
}

/*
* 这里为了正确接收前端的下划线风格,手动写了三个 setter 用于起别名
* 因为 Get 请求传入的参数不是 JSON,所以没办法使用 Jackson 的注解直接实现别名
* 添加 @JsonAlias 和 @JsonIgnore 注解只是为了保障 Swagger 的文档正确显示
* (吐槽一下,同样是Get请求,怎么CommentsQueries是驼峰命名,到了CopilotQueries就成了下划线命名)
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

因为后端是换语言重写开发的 评论区是现在java新开发的功能 其他则是继承之前C#的 (

@JsonIgnore
public void setLevel_keyword(String levelKeyword) {
this.levelKeyword = levelKeyword;
}

@JsonIgnore
public void setUploader_id(String uploaderId) {
this.uploaderId = uploaderId;
}

@JsonIgnore
public void setOrder_by(String orderBy) {
this.orderBy = orderBy;
}
}
66 changes: 61 additions & 5 deletions src/main/java/plus/maa/backend/repository/RedisCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
Expand All @@ -19,6 +26,7 @@
*
* @author AnselYuki
*/
@Slf4j
@Setter
@Component
@RequiredArgsConstructor
Expand All @@ -28,6 +36,15 @@ public class RedisCache {

private final StringRedisTemplate redisTemplate;

// 添加 JSR310 模块,以便顺利序列化 LocalDateTime 等类型
private final ObjectMapper writeMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
private final ObjectMapper readMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build();

public <T> void setData(final String key, T value) {
setCache(key, value, 0, TimeUnit.SECONDS);
}
Expand All @@ -43,7 +60,7 @@ public <T> void setCache(final String key, T value, long timeout) {
public <T> void setCache(final String key, T value, long timeout, TimeUnit timeUnit) {
String json;
try {
json = new ObjectMapper().writeValueAsString(value);
json = writeMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
return;
}
Expand Down Expand Up @@ -91,9 +108,9 @@ public <T> T getCache(final String key, Class<T> valueType, Supplier<T> onMiss,
return null;
}
}
result = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readValue(json, valueType);
result = readMapper.readValue(json, valueType);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
return null;
}
return result;
Expand All @@ -115,13 +132,13 @@ public <T> void updateCache(final String key, Class<T> valueType, T defaultValue
if (StringUtils.isEmpty(json)) {
result = defaultValue;
} else {
result = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readValue(json, valueType);
result = readMapper.readValue(json, valueType);
}
result = onUpdate.apply(result);
setCache(key, result, timeout, timeUnit);
}
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
}
}

Expand All @@ -136,4 +153,43 @@ public void setCacheLevelCommit(String commit) {
public void removeCache(String key) {
redisTemplate.delete(key);
}

/**
* 模糊删除缓存。
*
* @param pattern 待删除的 Key 表达式,例如 "home:*" 表示删除 Key 以 "home:" 开头的所有缓存
* @author Lixuhuilll
*/
public void removeCacheByPattern(String pattern) {
// 批量删除的阈值
final int batchSize = 10000;
// 构造 ScanOptions
ScanOptions scanOptions = ScanOptions.scanOptions()
.count(batchSize)
.match(pattern)
.build();

// 保存要删除的键
List<String> keysToDelete = new ArrayList<>();

// try-with-resources 自动关闭 SCAN
try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
while (cursor.hasNext()) {
String key = cursor.next();
// 将要删除的键添加到列表中
keysToDelete.add(key);

// 如果达到批量删除的阈值,则执行批量删除
if (keysToDelete.size() >= batchSize) {
redisTemplate.delete(keysToDelete);
keysToDelete.clear();
}
}
}

// 删除剩余的键(不足 batchSize 的最后一批)
if (!keysToDelete.isEmpty()) {
redisTemplate.delete(keysToDelete);
}
}
}
40 changes: 36 additions & 4 deletions src/main/java/plus/maa/backend/service/CopilotService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -161,6 +162,11 @@ public void delete(String loginUserId, CopilotCUDRequest request) {
Assert.state(Objects.equals(copilot.getUploaderId(), loginUserId), "您无法修改不属于您的作业");
copilot.setDelete(true);
copilotRepository.save(copilot);
/*
* 删除作业时,删除首页的所有缓存,因为此时首页内容可能发生变化
* 新增作业就不必,因为新作业显然不会那么快就登上热度榜和浏览量榜
*/
redisCache.removeCacheByPattern("home:*");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新作业不需要的话可以先加个简单的判断
缓存热度数据时将热度最小值同时放到缓存中,热度更新是一天一次的,且目前放在3点执行,很小概率会出现删除后由于缓存数据仍然驻留在页面上的情况
缓存浏览量数据时可以直接存入最小浏览量,这个数据比热度可靠

Copy link
Contributor Author

@Lixuhuilll Lixuhuilll Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

但是如果缓存时间够长,期间就可能出现用户看得到热度榜上的作业,但作业实际被作者删除了的情况,显然会给用户带来困扰。目前新的逻辑通过Redis的Set集合记录被缓存的作业ID,当删除的作业处于缓存中时,清空对应的缓存。

});
}

Expand Down Expand Up @@ -200,12 +206,31 @@ public Optional<CopilotInfo> getCopilotById(String userIdOrIpAddress, Long id) {

/**
* 分页查询。传入 userId 不为空时限制为用户所有的数据
* 会缓存默认状态下热度和访问量排序的结果
*
* @param userId 获取已登录用户自己的作业数据
* @param request 模糊查询
* @return CopilotPageInfo
*/
public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesRequest request) {
// 只缓存默认状态下热度和访问量排序的结果,并且最多只缓存前三页
AtomicReference<String> cacheKey = new AtomicReference<>();
if (request.getPage() <= 3 && request.getDocument() == null && request.getLevelKeyword() == null &&
request.getUploaderId() == null && request.getOperator() == null) {
Optional<CopilotPageInfo> cacheOptional = Optional.ofNullable(request.getOrderBy())
.filter(StringUtils::isNotBlank)
.map(ob -> switch (ob) {
case "hot", "views" -> {
cacheKey.set(String.format("home:%s:%s", ob, request.hashCode()));
yield redisCache.getCache(cacheKey.get(), CopilotPageInfo.class);
}
default -> null;
});
if (cacheOptional.isPresent()) {
return cacheOptional.get();
}
}

Sort.Order sortOrder = new Sort.Order(
request.isDesc() ? Sort.Direction.DESC : Sort.Direction.ASC,
Optional.ofNullable(request.getOrderBy())
Expand Down Expand Up @@ -278,13 +303,13 @@ public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesReq
}

// 封装查询
if (andQueries.size() > 0) {
if (!andQueries.isEmpty()) {
criteriaObj.andOperator(andQueries);
}
if (norQueries.size() > 0) {
if (!norQueries.isEmpty()) {
criteriaObj.norOperator(norQueries);
}
if (orQueries.size() > 0) {
if (!orQueries.isEmpty()) {
criteriaObj.orOperator(orQueries);
}
queryObj.addCriteria(criteriaObj);
Expand Down Expand Up @@ -317,11 +342,18 @@ public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesReq
boolean hasNext = count - (long) page * limit > 0;

// 封装数据
return new CopilotPageInfo()
CopilotPageInfo data = new CopilotPageInfo()
.setTotal(count)
.setHasNext(hasNext)
.setData(infos)
.setPage(pageNumber);

// 决定是否缓存
if (cacheKey.get() != null) {
// 缓存一小时
redisCache.setCache(cacheKey.get(), data, 3600);
}
return data;
}

/**
Expand Down