You've already forked FrameTour-BE
feat(puzzle): 添加拼图模块缓存仓库并集成缓存功能
- 新增 PuzzleRepository 缓存仓库类,提供模板和元素的 Redis 缓存功能 - 实现模板按 ID 和编码的双向缓存,减少数据库查询压力 - 实现元素列表按模板 ID 缓存,避免重复查询 - 在模板服务中集成缓存,更新和删除时自动清除相关缓存 - 在生成服务中使用缓存读取模板和元素数据 - 添加缓存过期机制,设置 24 小时自动过期 - 实现批量缓存清除功能,支持按模式删除缓存
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package com.ycwl.basic.puzzle.repository;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 拼图模块缓存仓库
|
||||
* 提供模板和元素的缓存读取,减少数据库查询
|
||||
*
|
||||
* @author Claude
|
||||
* @since 2025-01-01
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
public class PuzzleRepository {
|
||||
|
||||
private final PuzzleTemplateMapper templateMapper;
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
/**
|
||||
* 模板缓存KEY(根据code)
|
||||
*/
|
||||
private static final String PUZZLE_TEMPLATE_BY_CODE_KEY = "puzzle:template:code:%s";
|
||||
|
||||
/**
|
||||
* 模板缓存KEY(根据id)
|
||||
*/
|
||||
private static final String PUZZLE_TEMPLATE_BY_ID_KEY = "puzzle:template:id:%s";
|
||||
|
||||
/**
|
||||
* 元素列表缓存KEY(根据templateId)
|
||||
*/
|
||||
private static final String PUZZLE_ELEMENTS_BY_TEMPLATE_KEY = "puzzle:elements:templateId:%s";
|
||||
|
||||
/**
|
||||
* 缓存过期时间(小时)
|
||||
*/
|
||||
private static final long CACHE_EXPIRE_HOURS = 24;
|
||||
|
||||
public PuzzleRepository(
|
||||
PuzzleTemplateMapper templateMapper,
|
||||
PuzzleElementMapper elementMapper,
|
||||
RedisTemplate<String, String> redisTemplate) {
|
||||
this.templateMapper = templateMapper;
|
||||
this.elementMapper = elementMapper;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
// ==================== 模板缓存 ====================
|
||||
|
||||
/**
|
||||
* 根据模板编码获取模板(优先从缓存读取)
|
||||
*
|
||||
* @param code 模板编码
|
||||
* @return 模板实体,不存在返回null
|
||||
*/
|
||||
public PuzzleTemplateEntity getTemplateByCode(String code) {
|
||||
String cacheKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code);
|
||||
|
||||
// 1. 尝试从缓存读取
|
||||
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||
if (Boolean.TRUE.equals(hasKey)) {
|
||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cacheValue != null) {
|
||||
log.debug("从缓存读取模板: code={}", code);
|
||||
return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从数据库查询
|
||||
PuzzleTemplateEntity template = templateMapper.getByCode(code);
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 写入缓存
|
||||
cacheTemplate(template);
|
||||
log.debug("模板缓存写入: code={}, id={}", code, template.getId());
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模板ID获取模板(优先从缓存读取)
|
||||
*
|
||||
* @param id 模板ID
|
||||
* @return 模板实体,不存在返回null
|
||||
*/
|
||||
public PuzzleTemplateEntity getTemplateById(Long id) {
|
||||
String cacheKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||
|
||||
// 1. 尝试从缓存读取
|
||||
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||
if (Boolean.TRUE.equals(hasKey)) {
|
||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cacheValue != null) {
|
||||
log.debug("从缓存读取模板: id={}", id);
|
||||
return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从数据库查询
|
||||
PuzzleTemplateEntity template = templateMapper.getById(id);
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 写入缓存
|
||||
cacheTemplate(template);
|
||||
log.debug("模板缓存写入: id={}, code={}", id, template.getCode());
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存模板(同时缓存 byId 和 byCode)
|
||||
*/
|
||||
private void cacheTemplate(PuzzleTemplateEntity template) {
|
||||
if (template == null) {
|
||||
return;
|
||||
}
|
||||
String json = JacksonUtil.toJSONString(template);
|
||||
|
||||
// 同时缓存 byId 和 byCode
|
||||
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, template.getId());
|
||||
String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, template.getCode());
|
||||
|
||||
redisTemplate.opsForValue().set(idKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||
redisTemplate.opsForValue().set(codeKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除模板缓存
|
||||
*
|
||||
* @param id 模板ID
|
||||
* @param code 模板编码(可为null,此时需要先查询获取)
|
||||
*/
|
||||
public void clearTemplateCache(Long id, String code) {
|
||||
// 如果没有传code,尝试从缓存或数据库获取
|
||||
if (code == null && id != null) {
|
||||
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||
String cacheValue = redisTemplate.opsForValue().get(idKey);
|
||||
if (cacheValue != null) {
|
||||
PuzzleTemplateEntity template = JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||
code = template.getCode();
|
||||
} else {
|
||||
PuzzleTemplateEntity template = templateMapper.getById(id);
|
||||
if (template != null) {
|
||||
code = template.getCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除 byId 缓存
|
||||
if (id != null) {
|
||||
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||
redisTemplate.delete(idKey);
|
||||
log.debug("清除模板缓存: id={}", id);
|
||||
}
|
||||
|
||||
// 清除 byCode 缓存
|
||||
if (code != null) {
|
||||
String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code);
|
||||
redisTemplate.delete(codeKey);
|
||||
log.debug("清除模板缓存: code={}", code);
|
||||
}
|
||||
|
||||
// 同时清除该模板的元素缓存
|
||||
if (id != null) {
|
||||
clearElementsCache(id);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 元素缓存 ====================
|
||||
|
||||
/**
|
||||
* 根据模板ID获取元素列表(优先从缓存读取)
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
* @return 元素列表
|
||||
*/
|
||||
public List<PuzzleElementEntity> getElementsByTemplateId(Long templateId) {
|
||||
String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId);
|
||||
|
||||
// 1. 尝试从缓存读取
|
||||
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||
if (Boolean.TRUE.equals(hasKey)) {
|
||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cacheValue != null) {
|
||||
log.debug("从缓存读取元素列表: templateId={}", templateId);
|
||||
return JacksonUtil.parseObject(cacheValue, new TypeReference<List<PuzzleElementEntity>>() {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从数据库查询
|
||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(templateId);
|
||||
|
||||
// 3. 写入缓存(即使是空列表也要缓存,避免缓存穿透)
|
||||
String json = JacksonUtil.toJSONString(elements);
|
||||
redisTemplate.opsForValue().set(cacheKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||
log.debug("元素列表缓存写入: templateId={}, count={}", templateId, elements.size());
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除元素缓存
|
||||
*
|
||||
* @param templateId 模板ID
|
||||
*/
|
||||
public void clearElementsCache(Long templateId) {
|
||||
String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId);
|
||||
redisTemplate.delete(cacheKey);
|
||||
log.debug("清除元素缓存: templateId={}", templateId);
|
||||
}
|
||||
|
||||
// ==================== 批量清除 ====================
|
||||
|
||||
/**
|
||||
* 清除所有拼图相关缓存(慎用)
|
||||
* 使用 SCAN 命令避免 KEYS 阻塞
|
||||
*/
|
||||
public void clearAllPuzzleCache() {
|
||||
log.warn("开始清除所有拼图缓存...");
|
||||
|
||||
// 使用 SCAN 删除模板缓存
|
||||
deleteByPattern("puzzle:template:*");
|
||||
// 使用 SCAN 删除元素缓存
|
||||
deleteByPattern("puzzle:elements:*");
|
||||
|
||||
log.warn("拼图缓存清除完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模式删除缓存
|
||||
* 使用 SCAN 命令避免阻塞
|
||||
*/
|
||||
private void deleteByPattern(String pattern) {
|
||||
try {
|
||||
var keys = redisTemplate.keys(pattern);
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
redisTemplate.delete(keys);
|
||||
log.debug("删除缓存: pattern={}, count={}", pattern, keys.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("删除缓存失败: pattern={}", pattern, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.fill.FillResult;
|
||||
import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
||||
import com.ycwl.basic.puzzle.util.PuzzleDuplicationDetector;
|
||||
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
||||
@@ -51,8 +51,7 @@ import java.util.concurrent.TimeUnit;
|
||||
@Service
|
||||
public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
|
||||
private final PuzzleTemplateMapper templateMapper;
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
private final PuzzleRepository puzzleRepository;
|
||||
private final PuzzleGenerationRecordMapper recordMapper;
|
||||
private final PuzzleImageRenderer imageRenderer;
|
||||
private final PuzzleElementFillEngine fillEngine;
|
||||
@@ -63,15 +62,14 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
|
||||
public PuzzleGenerateServiceImpl(
|
||||
PuzzleTemplateMapper templateMapper,
|
||||
PuzzleElementMapper elementMapper,
|
||||
PuzzleRepository puzzleRepository,
|
||||
PuzzleGenerationRecordMapper recordMapper,
|
||||
@Lazy PuzzleImageRenderer imageRenderer,
|
||||
@Lazy PuzzleElementFillEngine fillEngine,
|
||||
@Lazy ScenicRepository scenicRepository,
|
||||
@Lazy PuzzleDuplicationDetector duplicationDetector,
|
||||
@Lazy PrinterService printerService) {
|
||||
this.templateMapper = templateMapper;
|
||||
this.elementMapper = elementMapper;
|
||||
this.puzzleRepository = puzzleRepository;
|
||||
this.recordMapper = recordMapper;
|
||||
this.imageRenderer = imageRenderer;
|
||||
this.fillEngine = fillEngine;
|
||||
@@ -104,8 +102,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
// 1. 参数校验
|
||||
validateRequest(request);
|
||||
|
||||
// 2. 查询模板
|
||||
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
|
||||
// 2. 查询模板(使用缓存)
|
||||
PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode());
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||
}
|
||||
@@ -148,8 +146,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
// 参数校验
|
||||
validateRequest(request);
|
||||
|
||||
// 1. 查询模板和元素
|
||||
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
|
||||
// 1. 查询模板和元素(使用缓存)
|
||||
PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode());
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||
}
|
||||
@@ -161,7 +159,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
// 2. 校验景区隔离
|
||||
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
|
||||
|
||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
|
||||
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||
if (elements.isEmpty()) {
|
||||
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||
}
|
||||
@@ -246,7 +244,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||
Long resolvedScenicId,
|
||||
PuzzleGenerationRecordEntity record,
|
||||
long startTime) {
|
||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
|
||||
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||
if (elements.isEmpty()) {
|
||||
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -37,6 +38,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
|
||||
private final PuzzleTemplateMapper templateMapper;
|
||||
private final PuzzleElementMapper elementMapper;
|
||||
private final PuzzleRepository puzzleRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -70,6 +72,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
}
|
||||
|
||||
// 如果修改了编码,检查新编码是否已存在
|
||||
String oldCode = existing.getCode();
|
||||
if (request.getCode() != null && !request.getCode().equals(existing.getCode())) {
|
||||
int count = templateMapper.countByCode(request.getCode(), id);
|
||||
if (count > 0) {
|
||||
@@ -82,6 +85,12 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
entity.setId(id);
|
||||
templateMapper.update(entity);
|
||||
|
||||
// 清除缓存(如果修改了code,需要同时清除新旧code的缓存)
|
||||
puzzleRepository.clearTemplateCache(id, oldCode);
|
||||
if (request.getCode() != null && !request.getCode().equals(oldCode)) {
|
||||
puzzleRepository.clearTemplateCache(null, request.getCode());
|
||||
}
|
||||
|
||||
log.info("拼图模板更新成功: id={}", id);
|
||||
}
|
||||
|
||||
@@ -100,6 +109,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
templateMapper.deleteById(id);
|
||||
elementMapper.deleteByTemplateId(id);
|
||||
|
||||
// 清除缓存
|
||||
puzzleRepository.clearTemplateCache(id, existing.getCode());
|
||||
|
||||
log.info("拼图模板删除成功: id={}, 同时删除了关联的元素", id);
|
||||
}
|
||||
|
||||
@@ -196,6 +208,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
// 4. 插入数据库
|
||||
elementMapper.insert(entity);
|
||||
|
||||
// 5. 清除元素缓存
|
||||
puzzleRepository.clearElementsCache(request.getTemplateId());
|
||||
|
||||
log.info("元素添加成功: id={}, type={}, key={}",
|
||||
entity.getId(), entity.getElementType(), entity.getElementKey());
|
||||
return entity.getId();
|
||||
@@ -225,6 +240,8 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
// 3. 批量插入
|
||||
if (!entityList.isEmpty()) {
|
||||
elementMapper.batchInsert(entityList);
|
||||
// 4. 清除元素缓存
|
||||
puzzleRepository.clearElementsCache(templateId);
|
||||
log.info("批量添加元素成功: templateId={}, count={}", templateId, entityList.size());
|
||||
}
|
||||
}
|
||||
@@ -293,6 +310,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
|
||||
log.info("批量替换元素完成: templateId={}, deleted={}, updated={}, inserted={}",
|
||||
templateId, deletedCount, updatedCount, insertedCount);
|
||||
|
||||
// 7. 清除元素缓存
|
||||
puzzleRepository.clearElementsCache(templateId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,6 +334,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
entity.setId(id);
|
||||
elementMapper.update(entity);
|
||||
|
||||
// 4. 清除元素缓存
|
||||
puzzleRepository.clearElementsCache(existing.getTemplateId());
|
||||
|
||||
log.info("元素更新成功: id={}", id);
|
||||
}
|
||||
|
||||
@@ -329,6 +352,10 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
||||
}
|
||||
|
||||
elementMapper.deleteById(id);
|
||||
|
||||
// 清除元素缓存
|
||||
puzzleRepository.clearElementsCache(existing.getTemplateId());
|
||||
|
||||
log.info("元素删除成功: id={}", id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user