使用 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
英文地址,請注明出處:http://en.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;讓媽媽和寶寶更安心。同時&…