1. 什么是滚动查询
滚动查询(Scroll Query)是一种高效获取大量数据的方式,尤其适用于需要分页加载但数据可能动态变化的场景。与传统分页相比,它能避免因数据新增 / 删除导致的分页偏移问题。
Redis 的 ZSet(有序集合)非常适合实现滚动查询,因为它具有以下特性:
- 元素带有分数 (score),可按分数排序
- 支持按分数范围查询
- 支持通过
ZRANGEBYSCORE
命令高效带有偏移量和数量限制的查询
2. 实现思路
- 使用 ZSet 的 score 字段存储排序依据(如时间戳、ID 等)
- 每次查询时,以上一次查询的最后一个元素的 score 作为偏移量
- 通过
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. 适用场景
- 社交媒体的时间线滚动加载
- 排行榜功能
- 日志记录的顺序查询
- 大数据量的有序列表展示