基于 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))
空值缓存 对查询结果为空的场景也进行短期缓存(防止频繁穿透)
限流降级 当缓存服务异常时,直接走数据库查询并记录告警
相关推荐
nlog3n19 分钟前
Go语言交替打印问题及多种实现方法
开发语言·算法·golang
kaixin_learn_qt_ing23 分钟前
Golang
开发语言·后端·golang
ddd...e_bug25 分钟前
Shell和Bash介绍
开发语言·bash
Chasing__Dreams1 小时前
Redis--基础知识点--26--过期删除策略 与 淘汰策略
数据库·redis·缓存
亚林瓜子1 小时前
Spring集成Redis中禁用主机名DNS检测
redis·spring·ssh
C4程序员1 小时前
Java百度身份证识别接口实现【配置即用】
java·开发语言
源远流长jerry1 小时前
MySQL的缓存策略
数据库·mysql·缓存
unityのkiven1 小时前
C++中的虚表和虚表指针的原理和示例
开发语言·c++
炒空心菜菜1 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
(・Д・)ノ1 小时前
python打卡day27
开发语言·python