Redis ZSet 实现滚动查询笔记

发布于:2025-09-10 ⋅ 阅读:(17) ⋅ 点赞:(0)
1. 什么是滚动查询

滚动查询(Scroll Query)是一种高效获取大量数据的方式,尤其适用于需要分页加载但数据可能动态变化的场景。与传统分页相比,它能避免因数据新增 / 删除导致的分页偏移问题。

Redis 的 ZSet(有序集合)非常适合实现滚动查询,因为它具有以下特性:

  • 元素带有分数 (score),可按分数排序
  • 支持按分数范围查询
  • 支持通过 ZRANGEBYSCORE 命令高效带有偏移量和数量限制的查询
2. 实现思路
  1. 使用 ZSet 的 score 字段存储排序依据(如时间戳、ID 等)
  2. 每次查询时,以上一次查询的最后一个元素的 score 作为偏移量
  3. 通过 ZRANGEBYSCORE 命令获取指定范围的数据
3. 代码实现
3.1 依赖配置

首先确保 pom.xml 中包含 Redis 依赖:

xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 Redis 配置类

java

运行

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 设置序列化方式
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
                ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        
        return template;
    }
}
3.3 滚动查询服务类

Redis ZSet 滚动查询服务实现

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ScrollQueryService {
    private final RedisTemplate<String, Object> redisTemplate;
    private static final String ZSET_KEY = "article:rank"; // 示例:文章排序集合

    public ScrollQueryService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 向ZSet中添加元素
     * @param member 元素值
     * @param score 排序分数(如时间戳)
     */
    public Boolean addToZSet(Object member, double score) {
        return redisTemplate.opsForZSet().add(ZSET_KEY, member, score);
    }

    /**
     * 滚动查询
     * @param lastScore 上一次查询的最后一个元素的分数,首次查询传0
     * @param pageSize 每页大小
     * @return 包含查询结果和最后一个元素分数的Map
     */
    public Map<String, Object> scrollQuery(double lastScore, int pageSize) {
        // 查询大于lastScore的元素,最多返回pageSize+1个(多查一个用于判断是否有下一页)
        Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet()
                .rangeByScoreWithScores(ZSET_KEY, lastScore + 1, Double.MAX_VALUE, 0, pageSize + 1);
        
        if (tuples == null || tuples.isEmpty()) {
            return Collections.emptyMap();
        }
        
        List<Object> result = new ArrayList<>();
        double newLastScore = lastScore;
        boolean hasMore = tuples.size() > pageSize;
        
        List<ZSetOperations.TypedTuple<Object>> list = new ArrayList<>(tuples);
        // 如果有多余元素,移除最后一个
        if (hasMore) {
            list = list.subList(0, pageSize);
        }
        
        // 提取结果和最后一个元素的分数
        for (ZSetOperations.TypedTuple<Object> tuple : list) {
            result.add(tuple.getValue());
            newLastScore = tuple.getScore();
        }
        
        Map<String, Object> response = new HashMap<>();
        response.put("data", result);
        response.put("lastScore", newLastScore);
        response.put("hasMore", hasMore);
        
        return response;
    }
    
    /**
     * 获取元素的分数
     */
    public Double getScore(Object member) {
        return redisTemplate.opsForZSet().score(ZSET_KEY, member);
    }
    
    /**
     * 删除元素
     */
    public Long remove(Object... members) {
        return redisTemplate.opsForZSet().remove(ZSET_KEY, members);
    }
}

创建时间:09:45

3.4 控制器使用示例

java

运行

@RestController
@RequestMapping("/api/articles")
public class ArticleController {
    private final ScrollQueryService scrollQueryService;

    public ArticleController(ScrollQueryService scrollQueryService) {
        this.scrollQueryService = scrollQueryService;
    }

    // 模拟添加文章
    @PostMapping
    public ResponseEntity<?> addArticle(@RequestBody Article article) {
        // 使用时间戳作为score,确保新文章排在前面
        double score = System.currentTimeMillis();
        scrollQueryService.addToZSet(article, score);
        return ResponseEntity.ok("添加成功");
    }

    // 滚动查询文章
    @GetMapping("/scroll")
    public ResponseEntity<?> scrollArticles(
            @RequestParam(defaultValue = "0") double lastScore,
            @RequestParam(defaultValue = "10") int pageSize) {
        return ResponseEntity.ok(scrollQueryService.scrollQuery(lastScore, pageSize));
    }
}
4. 关键命令解析
  • ZADD key score member:向有序集合添加元素
  • ZRANGEBYSCORE key min max [LIMIT offset count]:按分数范围查询元素
  • ZSCORE key member:获取元素的分数
  • ZREM key member:删除元素
5. 优缺点分析

优点

  • 性能优异,查询时间复杂度为 O (logN + M),N 为集合大小,M 为返回元素数
  • 避免传统分页的偏移问题,适合动态数据
  • 实现简单,无需复杂的游标管理

缺点

  • 依赖分数排序,不适合多条件排序场景
  • 无法直接跳转到指定页,只能顺序滚动
  • 数据存在 Redis 中,需要考虑与数据库的同步问题
6. 适用场景
  • 社交媒体的时间线滚动加载
  • 排行榜功能
  • 日志记录的顺序查询
  • 大数据量的有序列表展示

网站公告

今日签到

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