基于 RedisTemplate 的分页缓存设计

场景:某后台查询业务涵盖分页+条件搜索,那么我们需要设计一个缓存来有效存储检索数据,且基于 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))
空值缓存 对查询结果为空的场景也进行短期缓存(防止频繁穿透)
限流降级 当缓存服务异常时,直接走数据库查询并记录告警
相关推荐
我很好我还能学3 分钟前
【面试篇 9】c++生成可执行文件的四个步骤、悬挂指针、define和const区别、c++定义和声明、将引用作为返回值的好处、类的四个缺省函数
开发语言·c++
程序员JerrySUN16 分钟前
[特殊字符] 深入理解 Linux 内核进程管理:架构、核心函数与调度机制
java·linux·架构
2302_8097983219 分钟前
【JavaWeb】Docker项目部署
java·运维·后端·青少年编程·docker·容器
蓝婷儿25 分钟前
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
开发语言·python·学习
渣渣盟41 分钟前
基于Scala实现Flink的三种基本时间窗口操作
开发语言·flink·scala
zhojiew43 分钟前
关于akka官方quickstart示例程序(scala)的记录
后端·scala
网安INF43 分钟前
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
java·web安全·网络安全·flink·漏洞
一叶知秋哈43 分钟前
Java应用Flink CDC监听MySQL数据变动内容输出到控制台
java·mysql·flink
jackson凌1 小时前
【Java学习笔记】SringBuffer类(重点)
java·笔记·学习
sclibingqing1 小时前
SpringBoot项目接口集中测试方法及实现
java·spring boot·后端