基于 RedisTemplate 的分页缓存设计

发布于:2025-05-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

场景:某后台查询业务涵盖分页+条件搜索,那么我们需要设计一个缓存来有效存储检索数据,且基于 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))
空值缓存 对查询结果为空的场景也进行短期缓存(防止频繁穿透)
限流降级 当缓存服务异常时,直接走数据库查询并记录告警

网站公告

今日签到

点亮在社区的每一天
去签到