使用 StringRedisTemplate 实现 ZSet 滚动查询(处理相同分数场景)

1. 为什么需要改进

当 ZSet 中存在相同分数 (score) 的元素时,单纯使用分数作为偏移会导致数据漏查或重复。例如:

  • 多条记录具有相同时间戳(作为分数)
  • 分页查询时可能跳过相同分数的元素
  • 或重复查询相同分数的元素

改进方案:结合最后分数 (lastScore) 和相同分数内的偏移量 (offset) 实现精确滚动。

2. 实现代码
2.1 依赖配置

确保 pom.xml 包含 Redis 依赖:

xml

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2 服务实现类

使用StringRedisTemplate的高级ZSet滚动查询

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import java.util.*;@Service
public class ZSetScrollService {private final StringRedisTemplate stringRedisTemplate;// ZSet键名(可根据业务调整)private static final String ZSET_KEY = "articles:zset";public ZSetScrollService(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 高级滚动查询(处理相同分数场景)* @param lastScore 上一次查询的最后分数,首次查询传0* @param offset 相同分数内的偏移量,首次查询传0* @param pageSize 每页大小* @return 包含数据、最后分数、偏移量和是否有更多数据的Map*/public Map<String, Object> scrollQuery(double lastScore, int offset, int pageSize) {List<String> result = new ArrayList<>();double newLastScore = lastScore;int newOffset = 0;boolean hasMore = false;// 1. 处理上一次最后分数的剩余元素(如果有偏移)if (offset > 0) {// 获取与lastScore相同分数的所有元素Set<ZSetOperations.TypedTuple<String>> sameScoreTuples = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(ZSET_KEY, lastScore, lastScore);if (sameScoreTuples != null && !sameScoreTuples.isEmpty()) {List<ZSetOperations.TypedTuple<String>> sameScoreList = new ArrayList<>(sameScoreTuples);int remaining = sameScoreList.size() - offset;// 相同分数下还有剩余元素if (remaining > 0) {int take = Math.min(remaining, pageSize);// 从偏移位置开始取元素for (int i = offset; i < offset + take; i++) {result.add(sameScoreList.get(i).getValue());}newOffset = offset + take;// 如果已取完当前分数的所有元素,重置偏移if (newOffset >= sameScoreList.size()) {newOffset = 0;} else {newLastScore = lastScore;}// 如果取的元素小于pageSize,继续从更高分数取if (take < pageSize) {pageSize -= take;} else {// 已取够一页,判断是否有更多数据hasMore = checkHasMore(newLastScore, newOffset);return createResponse(result, newLastScore, newOffset, hasMore);}} else {newOffset = 0; // 相同分数下已无元素,重置偏移}} else {newOffset = 0; // 该分数已无元素,重置偏移}}// 2. 查询更高分数的元素if (pageSize > 0) {// 查询大于lastScore的元素,多查一个用于判断是否有下一页Set<ZSetOperations.TypedTuple<String>> higherTuples = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(ZSET_KEY, lastScore + 1, Double.MAX_VALUE, 0, pageSize + 1);if (higherTuples != null && !higherTuples.isEmpty()) {List<ZSetOperations.TypedTuple<String>> higherList = new ArrayList<>(higherTuples);hasMore = higherList.size() > pageSize;// 确定需要取的元素数量int take = hasMore ? pageSize : higherList.size();// 提取元素for (int i = 0; i < take; i++) {ZSetOperations.TypedTuple<String> tuple = higherList.get(i);result.add(tuple.getValue());newLastScore = tuple.getScore();}// 处理最后一个分数的偏移if (take > 0) {ZSetOperations.TypedTuple<String> lastTuple = higherList.get(take - 1);double currentScore = lastTuple.getScore();// 计算当前分数下的总元素数long totalSameScore = stringRedisTemplate.opsForZSet().count(ZSET_KEY, currentScore, currentScore);// 计算当前分数下已返回的元素数long currentScoreReturned = 0;for (int i = 0; i < take; i++) {if (higherList.get(i).getScore().equals(currentScore)) {currentScoreReturned++;}}// 设置相同分数内的偏移newOffset = (currentScoreReturned < totalSameScore) ? (int) currentScoreReturned : 0;}}}// 最终判断是否有更多数据if (!hasMore) {hasMore = checkHasMore(newLastScore, newOffset);}return createResponse(result, newLastScore, newOffset, hasMore);}/*** 检查是否有更多数据*/private boolean checkHasMore(double lastScore, int offset) {// 1. 检查当前分数下是否还有未返回的元素if (offset > 0) {Long sameScoreCount = stringRedisTemplate.opsForZSet().count(ZSET_KEY, lastScore, lastScore);if (sameScoreCount != null && sameScoreCount > offset) {return true;}}// 2. 检查是否存在更高分数的元素Long higherCount = stringRedisTemplate.opsForZSet().count(ZSET_KEY, lastScore + 1, Double.MAX_VALUE);return higherCount != null && higherCount > 0;}/*** 创建响应结果*/private Map<String, Object> createResponse(List<String> data, double lastScore, int offset, boolean hasMore) {Map<String, Object> response = new HashMap<>(4);response.put("data", data);response.put("lastScore", lastScore);response.put("offset", offset);response.put("hasMore", hasMore);return response;}// ------------------------- 辅助方法 -------------------------/*** 向ZSet添加元素* @param member 元素值(通常为JSON字符串)* @param score 排序分数(如时间戳)*/public Boolean add(String member, double score) {return stringRedisTemplate.opsForZSet().add(ZSET_KEY, member, score);}/*** 获取元素的分数*/public Double getScore(String member) {return stringRedisTemplate.opsForZSet().score(ZSET_KEY, member);}/*** 删除元素*/public Long remove(String... members) {return stringRedisTemplate.opsForZSet().remove(ZSET_KEY, members);}/*** 统计分数范围内的元素数量*/public Long count(double min, double max) {return stringRedisTemplate.opsForZSet().count(ZSET_KEY, min, max);}
}

创建时间:10:10

2.3 控制器使用示例

java

运行

@RestController
@RequestMapping("/api/zset")
public class ZSetController {private final ZSetScrollService scrollService;private final ObjectMapper objectMapper; // 用于JSON序列化public ZSetController(ZSetScrollService scrollService, ObjectMapper objectMapper) {this.scrollService = scrollService;this.objectMapper = objectMapper;}// 添加元素示例(对象转JSON字符串)@PostMapping("/add")public ResponseEntity<?> addElement(@RequestBody Article article) throws JsonProcessingException {// 将对象转为JSON字符串存储String jsonStr = objectMapper.writeValueAsString(article);// 使用时间戳作为分数(确保新元素排在前面可使用负的时间戳)double score = System.currentTimeMillis();scrollService.add(jsonStr, score);return ResponseEntity.ok("添加成功");}// 滚动查询示例@GetMapping("/scroll")public ResponseEntity<?> scroll(@RequestParam(defaultValue = "0") double lastScore,@RequestParam(defaultValue = "0") int offset,@RequestParam(defaultValue = "10") int pageSize) throws JsonProcessingException {Map<String, Object> result = scrollService.scrollQuery(lastScore, offset, pageSize);// 将JSON字符串转为对象(可选)if (result.containsKey("data")) {List<String> jsonList = (List<String>) result.get("data");List<Article> articles = jsonList.stream().map(json -> {try {return objectMapper.readValue(json, Article.class);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}).collect(Collectors.toList());result.put("data", articles);}return ResponseEntity.ok(result);}
}
3. 核心原理说明
  1. 双重定位机制

    • lastScore:记录上一次查询的最后一个元素的分数
    • offset:记录在该分数下已经返回的元素数量,解决相同分数问题
  2. 查询流程

    • 先处理上一次最后分数中未返回的元素(基于 offset)
    • 再查询更高分数的元素,直到满足分页大小
    • 自动计算下一次查询所需的 lastScore 和 offset
  3. 关键 Redis 命令

    • ZRANGEBYSCORE key min max [LIMIT offset count]:按分数范围查询
    • ZCOUNT key min max:统计分数范围内的元素数量
    • ZSCORE key member:获取元素的分数
4. 使用场景
  • 社交媒体时间线(按发布时间排序,可能有相同时间)
  • 实时排行榜(按分数排序,可能有相同分数)
  • 日志记录查询(按时间戳排序)
  • 大数据量有序列表的滚动加载

这种实现既保留了 Redis ZSet 的高性能,又解决了相同分数元素的查询问题,是处理动态有序数据滚动加载的理想方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/95946.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/95946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Android】安装2025版AndroidStudio开发工具开发老安卓旧版App

为了开发老旧的安卓App&#xff0c;这里记录一下2025版AndroidStudio的安装过程&#xff0c;如果卸载以后&#xff0c;可以按照此文章的步骤顺利重新安装继续使用。 文章目录安装包Android SDK新建项目新建页面构建项目Gradle下载失败构建失败构建完成编译失败安装失败关于APP在…

Python跳过可迭代对象前部元素完全指南:从基础到高并发系统实战

引言&#xff1a;跳过前部元素的核心价值在数据处理和系统开发中&#xff0c;跳过可迭代对象的前部元素是常见且关键的操作。根据2024年数据处理报告&#xff1a;92%的数据清洗需要跳过文件头部85%的日志分析需要忽略初始记录78%的网络协议处理需跳过头部信息65%的机器学习训练…

ConcurrentHashMap扩容机制

ConcurrentHashMap的扩容为了提高效率&#xff0c;是多线程并发的每个线程控制一部分范围节点的扩容(根据cpu与数组长度确定控制多大范围)有两个核心参数sizeCtl&#xff1a;标记扩容状态 负数时代表正在扩容&#xff0c;存储量参与扩容的线程数&#xff0c;正数代表出发扩容的…

Spring Cloud Gateway 进行集群化部署

如果将 Gateway 单独部署为一个服务而不做任何高可用处理&#xff0c;它确实会成为一个单点故障&#xff08;SPOF, Single Point of Failure&#xff09;。如果这个唯一的 Gateway 实例因为服务器宕机、应用崩溃、部署更新或其他任何原因而不可用&#xff0c;那么整个系统的所有…

计算机网络:以太网中的数据传输

以太网中&#xff0c;数据的传输依赖于一系列标准化的技术规范&#xff0c;核心包括帧结构封装、介质访问控制机制和物理层编码技术&#xff0c;具体如下&#xff1a; 1. 以“帧&#xff08;Frame&#xff09;”为基本传输单元 以太网在数据链路层将网络层的数据包&#xff08;…

元器件--USB TypC接口

USB TypC接口下图这些都是USB接口A口与B口的区别USB A口和B口最初由USB-IF在1996年引入。根据当时的USB协议&#xff0c;A口主要用于主设备&#xff08;如电脑&#xff09;&#xff0c;而B口则用于从设备&#xff08;如打印机和摄像头&#xff09;。随着USB-C接口的日益普及&am…

多线程之HardCodedTarget(type=OssFileClient, name=file, url=http://file)异常

多线程之HardCodedTarget(typeOssFileClient, namefile, urlhttp://file)异常 摘要&#xff1a; 文档描述了多线程环境下调用Feign客户端OssFileClient时出现的HardCodedTarget异常。异常发生在异步保存文件到ES时&#xff0c;Feign调用未返回预期结果而直接打印了客户端对象。…

计算机视觉(十二):人工智能、机器学习与深度学习

人工智能 (AI)&#xff1a;宏大的目标 人工智能是最广泛、最宏大的概念&#xff0c;它的目标是让机器能够模仿人类的智能行为&#xff0c;例如&#xff1a; 推理&#xff1a;像下棋程序一样&#xff0c;通过逻辑来做决策。规划&#xff1a;为实现一个目标而制定步骤&#xff0c…

容器元素的滚动条回到顶部

关闭再打开后&#xff0c;容器元素的滚动条回到顶部解决方法&#xff1a;1、通过打开开发者工具&#xff08;F12&#xff09;&#xff0c;找到滚动条所属元素为 el-textarea__inner&#xff0c;其父类 class"el-textarea content"2、代码&#xff0c;通过元素的方法 …

分布式专题——2 深入理解Redis线程模型

1 Redis 简介 1.1 Redis 是什么&#xff1f; Redis 全称 Remote Dictionary Server&#xff08;远程字典服务&#xff09;&#xff0c;是一个开源的高性能 Key-Value 数据库&#xff1b; 官网&#xff1a;Redis - The Real-time Data Platform&#xff1b; 引用官网上的⼀个…

simd学习

如何查看cpu是否支持simd&#xff1f;# 检查特定指令集 grep -o avx2 /proc/cpuinfo | head -1 # 检查AVX2 grep -o sse4 /proc/cpuinfo | head -1 # 检查SSE4 grep -o avx512 /proc/cpuinfo | head -1 # 检查AVX512gcc编译选项&#xff0c;增加支持simd-mavx2 -D__AVX2__SS…

LabVIEW汽车发动机振动测试

以某型号四缸汽油发动机为测试对象&#xff0c;借助 LabVIEW 平台与高精度数据采集硬件&#xff0c;开展发动机全工况振动测试。通过实时采集缸体、曲轴箱关键部位振动信号&#xff0c;分析振动特征与故障关联&#xff0c;验证发动机运行稳定性&#xff0c;为后期优化设计提供数…

android 四大组件—Service

启动服务startService//启动服务&#xff0c;通过类名 Intent intent new Intent(this, WiFiAutoLinkService.class); startService(intent); //通过字符串启动 Intent intent new Intent(); intent.setAction("com.launcher.app"); intent.setPackage("com.l…

https + 域名 + 客户端证书访问模式

项目使用金融云部署&#xff0c;对外暴露IP访问&#xff0c;因安全合规要求必须使用域名访问&#xff0c;但公司又不提供域名。故&#xff0c;改为 https 域名 客户端证书双向认证 访问模式&#xff0c;大大提升安全性。 1. 密钥文件类型 .key、.csr、.cer&#xff08;或 .cr…

ICPC 2023 Nanjing R L 题 Elevator

[ProblemDiscription]\color{blue}{\texttt{[Problem Discription]}}[Problem Discription] 来源&#xff1a;洛谷。侵权则删。 [Analysis]\color{blue}{\texttt{[Analysis]}}[Analysis] 贪心。优先运送楼层高的货物&#xff0c;在能装下的情况下尽量多装。 因为运送货物的代价…

81-dify案例分享-零代码用 Dify 使用梦 AI 3.0 多模态模型,免费生成影视级视频

1.前言 即梦AI作为字节跳动旗下的AI绘画与视频生成平台&#xff0c;近年来不断推出新的模型和功能&#xff0c;以提升用户体验和创作能力。 即梦AI 3.0是即梦AI的最新版本&#xff0c;于2025年4月发布&#xff0c;标志着其在中文生图模型上的重大升级。该版本不仅在中文生图能…

SQL 进阶指南:视图的创建与使用(视图语法 / 作用 / 权限控制)

在 SQL 操作中&#xff0c;你是否遇到过 “频繁查询多表关联的固定结果”“不想让他人看到表中的敏感字段” 这类问题&#xff1f;比如 “每周都要查‘技术部员工的姓名、职位、薪资’”&#xff0c;每次都写多表关联语句很麻烦&#xff1b;又比如 “给实习生开放数据查询权限&…

【全部更新完毕】2025数学建模国赛C题思路代码文章高教社杯全国大学生数学建模-NIPT 的时点选择与胎儿的异常判定

B题全部更新完毕 包含完整的文章全部问题的代码、结果、图表 完整内容请看文末最后的推广群NIPT 的时点选择与胎儿的异常判定 摘要 在问题一中&#xff0c;我们以无创产前检测&#xff08;NIPT&#xff09;数据为研究对象&#xff0c;围绕“胎儿 Y 染色体浓度”(记为 (V)) 随孕…

Redis(43)Redis哨兵(Sentinel)是什么?

Redis Sentinel&#xff08;哨兵&#xff09;是一种用于管理 Redis 实例的高可用性解决方案。它提供了监控、通知和自动故障转移等功能&#xff0c;确保 Redis 服务在发生故障时能够自动恢复&#xff0c;提供高可用性和可靠性。以下是详细介绍 Redis Sentinel 的功能及其代码示…

蓓韵安禧DHA纯植物藻油纯净安全零添加守护母婴健康

在母婴健康领域&#xff0c;选择合适的营养补充品至关重要。纯植物藻油DHA源自纯净藻类&#xff0c;有效规避了海洋重金属污染的风险&#xff0c;确保安全无隐患。配方坚持零添加香精、色素和防腐剂&#xff0c;避免不必要的化学物质摄入&#xff0c;让妈妈和宝宝更安心。同时&…