场景:某后台查询业务涵盖分页+条件搜索,那么我们需要设计一个缓存来有效存储检索数据,且基于 RedisTemplate 的分页缓存设计
核心:分页缓存键设计,我需要考虑如何将查询条件转化为缓存键的一部分。通常,处理这种情况的方法是对查询条件进行哈希处理,生成一个唯一的字符串作为键的一部分。这样,不同的查询条件会有不同的哈希值,从而避免键的冲突。例如,用户可能有多个查询参数,如作者、状态、日期范围等,这些参数组合起来应该生成唯一的键
设计规范:模块名:业务类型:页码:页大小:条件哈希
我们自定义RedisUtil工具,此工具功能
- 统一缓存键(key)的创建格式
- 删除缓存键(key)
/**
* Redis统一键命名规范
* 分页缓存(键类型) 模块名:业务类型:page_{页码}_size_{页数}_queryhash 如 user:list:page_1_size_10_abcd123
* 详情缓存(键类型)模块名:业务类型:id_{ID} 如 user:detail:id_1001
* 统计缓存(键类型)模块名:statistics:类型 如 order:statistics:daily
*/
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 清理指定模块的所有缓存
* @param module 模块名(如 "user", "order")
*/
public void cleanModuleCache(String module) {
deleteByPattern(module + ":*");
}
/**
* 清理模块下特定业务类型缓存
* @param module 模块名
* @param bizType 业务类型(如 "list", "detail")
*/
public void cleanBizTypeCache(String module, String bizType) {
deleteByPattern(module + ":" + bizType + ":*");
}
/**
* 通用清理方法(支持任意模式)
* @param pattern
* @return
*/
public long deleteByPattern(String pattern) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
List<byte[]> keysToDelete = new ArrayList<>();
ScanOptions options = ScanOptions.scanOptions()
.match(pattern)
.count(500) // 每批扫描500个键
.build();
connection.scan(options).forEachRemaining(keyBytes -> {
String key = new String(keyBytes, StandardCharsets.UTF_8);
// 添加额外验证逻辑(可选)
if (isValidKey(key)) {
keysToDelete.add(keyBytes);
}
});
if (!keysToDelete.isEmpty()) {
connection.del(keysToDelete.toArray(new byte[0][]));
}
return (long) keysToDelete.size();
});
}
/**
* 验证键格式合法性(防止误删)
* @param key
* @return
*/
private boolean isValidKey(String key) {
// 示例验证:必须包含至少两级分类(如 "user:list:*")
return key.matches("^\\w+:\\w+:.*");
}
/**
* 生成分页缓存键, 如: user:list:1:10:abcd123
* @param module
* @param page
* @param size
* @param query
* @return
*/
public String generatePageKey(String module, int page, int size, Object query) {
String queryHash = generateConditionHash(query);
return String.format("%s:%d:%d:%s", module, page, size, queryHash);
}
/**
* 生成条件哈希值
* @param query
* @return
*/
private String generateConditionHash(Object query) {
if (query == null) return "no_condition";
try {
String json = new ObjectMapper().writeValueAsString(query);
return DigestUtils.md5DigestAsHex(json.getBytes());
} catch (JsonProcessingException e) {
throw new RuntimeException("生成条件哈希失败", e);
}
}
}
通过上面的RedisUtil工具,我们将缓存键场景通过下面列表进行总结
场景 | 缓存键示例 | 操作流程 |
---|---|---|
基础分页查询 | TrainManageCache:1:10:no_condition |
直接使用页码和分页大小生成键 |
带状态过滤的分页 | TrainManageCache:2:20:d3e5f7a9 |
将查询条件序列化为哈希值 |
多条件复杂查询 | TrainManageCache:1:10:8c2b4a6d |
确保所有条件参数参与哈希计算 |
排序分页 | TrainManageCache:3:15:7e9f1d3a |
包含排序字段和方向的哈希值 |
示例:我们能也可以扩展一个分页缓存工具
@Component
public class PageCacheUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 写入分页缓存
* @param key 缓存键
* @param page 分页数据对象(需包含 total 等元数据)
* @param ttl 过期时间(单位:分钟)
*/
public void setPageCache(String key, Page<?> page, long ttl) {
// 使用 GenericJackson2JsonRedisSerializer 确保类型信息保留
redisTemplate.opsForValue().set(
key,
page,
Duration.ofMinutes(ttl)
);
}
/**
* 读取分页缓存
* @param key 缓存键
* @return Page 对象(反序列化失败返回 null)
*/
public Page<?> getPageCache(String key) {
try {
return (Page<?>) redisTemplate.opsForValue().get(key);
} catch (Exception e) {
// 处理反序列化异常(如旧数据格式不兼容)
return null;
}
}
}
业务层调用
@Service
public class TrainService {
@Autowired
private TrainMapper trainMapper;
@Autowired
private RedisTemplate redisTemplate;
private static final String CACHE_MODULE = "TrainManageCache";
private static final int DEFAULT_TTL = 30; // 缓存30分钟
/**
* 分页查询(带缓存逻辑)
*/
public Page<Train> queryTrainPage(int page, int size, TrainQuery query) {
RedisUtil redisUtil = new RedisUtil(redisTemplate);
// 生成缓存键
String cacheKey = redisUtil.generatePageKey(
CACHE_MODULE, page, size, query
);
// 尝试读取缓存
Page<Train> cachedPage = (Page<Train>) pageCacheUtil.getPageCache(cacheKey);
if (cachedPage != null) return cachedPage;
// 缓存未命中,查询数据库
PageHelper.startPage(page, size);
List<Train> data = trainMapper.selectByQuery(query);
Page<Train> resultPage = (Page<Train>) data;
// 写入缓存
pageCacheUtil.setPageCache(cacheKey, resultPage, DEFAULT_TTL);
return resultPage;
}
}
安全与优化
优化项 | 实现方式 |
---|---|
空条件处理 | 对无查询条件的情况生成统一哈希(no_condition ) |
动态 TTL | 根据查询频率设置不同过期时间(高频查询设置更长 TTL) |
防雪崩策略 | 对缓存设置随机偏移的过期时间(如 ttl + random.nextInt(10) ) |
空值缓存 | 对查询结果为空的场景也进行短期缓存(防止频繁穿透) |
限流降级 | 当缓存服务异常时,直接走数据库查询并记录告警 |