feat(puzzle): 添加拼图模块缓存仓库并集成缓存功能

- 新增 PuzzleRepository 缓存仓库类,提供模板和元素的 Redis 缓存功能
- 实现模板按 ID 和编码的双向缓存,减少数据库查询压力
- 实现元素列表按模板 ID 缓存,避免重复查询
- 在模板服务中集成缓存,更新和删除时自动清除相关缓存
- 在生成服务中使用缓存读取模板和元素数据
- 添加缓存过期机制,设置 24 小时自动过期
- 实现批量缓存清除功能,支持按模式删除缓存
This commit is contained in:
2026-01-01 21:39:43 +08:00
parent f8374519c3
commit 21d8c56e82
3 changed files with 298 additions and 13 deletions

View File

@@ -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);
}
}
}

View File

@@ -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());
}

View File

@@ -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);
}