Redis SCAN 命令的詳細介紹
以下是 Redis SCAN
? 命令的詳細介紹,結合其核心特性、使用場景及底層原理進行綜合說明:
工作原理圖 :
?
一、核心特性
-
非阻塞式迭代
- 通過游標(Cursor) 分批次遍歷鍵,避免一次性全量掃描阻塞主線程。
- 每次迭代僅返回少量數據(默認約 10 個鍵),分散服務器壓力。
-
弱一致性保證
- 迭代過程中若鍵被修改(新增/刪除),可能導致重復或遺漏。
- 采用快照機制,但無法保證強一致性,需業務層處理重復數據。
-
支持模式匹配與類型過濾
- ?
MATCH
?? 參數支持通配符(如user:*
?)過濾鍵名。 - ?
TYPE
? 參數(Redis 6.0+)可指定鍵類型(如hash
??、string
??)。
- ?
二、命令語法與參數
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
- **
cursor
?**
初始值為0
?,后續使用前次返回的新游標。當游標返回0
? 時,迭代結束。 - **
COUNT
?**
建議單次返回的鍵數量(默認 10),但實際結果可能多于或少于該值。
(例:??COUNT 1000
?? 提示 Redis 嘗試每批返回約 1000 個鍵) 。
三、底層原理
-
高位進位加法遍歷
- 通過二進制高位進位順序遍歷字典槽(Slot),避免擴容/縮容導致的數據遺漏或重復。
- 例如:從
0000
? →1000
? →0100
? →1100
?,確保新舊哈希表遍歷順序連續。
-
字典擴容與漸進式 Rehash
- 擴容時新舊哈希表共存,
SCAN
? 會同時遍歷兩個表,保證數據完整性。 - 縮容可能導致部分鍵被重復掃描,需客戶端去重。
- 擴容時新舊哈希表共存,
四、使用場景
-
生產環境大數據量遍歷
- 替代
KEYS
? 命令,避免因全量掃描導致服務阻塞。 - 示例:遍歷百萬級用戶會話鍵(
session:*
?)進行清理。
- 替代
-
數據結構專用迭代
- ?
SSCAN
?(集合)、HSCAN
?(哈希)、ZSCAN
?(有序集合)支持按類型迭代元素。
- ?
-
模糊查詢與分頁
- 結合
MATCH
? 實現模糊匹配,利用 ?COUNT
?? 近似分頁控制返回量。
- 結合
五、注意事項
-
重復鍵處理
- 迭代期間鍵空間變動可能導致重復結果,需客戶端去重。
-
COUNT 參數優化
- 根據數據規模調整
COUNT
? 值(如 1000~10000),平衡網絡往返次數與單次負載。
- 根據數據規模調整
-
弱一致性的影響
- 不適用于需精確統計的場景(如實時計數),建議改用其他方案(如維護索引集合)。
使用案例:
從redis中取出數據同步到后臺的其他持久化數據庫 demo 這種分批掃描的方法可以避免一次返回大量 key 而導致 Redis 阻塞,同時可以根據需要對每批數據進行處理
package com.example.scan;/*** 描述: 從 Redis 批量獲取暫存數據并持久化到數據庫。通過SCAN分批拉取數據,確保系統穩定性* 1. 循環獲取指定開頭的key* 2. 判斷key 的數據類型* 3. 對不同的數據類型做相應的處理* 4. 這里模擬如果是string 同步到數據庫 其他只是簡單的打印 后續可以根據業務場景的不通 做不同的處理* @author ZHOUXIAOYUE* @date 2025/4/21 10:20*/import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;import java.util.List;
import java.util.Map;
import java.util.Set;public class RedisDataMigration {public static void main(String[] args) {// 初始化 Jedis 客戶端Jedis jedis = new Jedis("localhost", 6379);// SCAN 命令參數設置// count 參數表示每次掃描大概處理多少條數據,這里設置為 100ScanParams scanParams = new ScanParams().match("user:*").count(100);// 如果需要,可以通過 match 設置鍵模式// scanParams.match("temp:*");String cursor = "0";do {// 執行 SCAN 命令ScanResult<String> scanResult = jedis.scan(cursor, scanParams);List<String> keys = scanResult.getResult();cursor = scanResult.getCursor();// 模擬持久化到數據庫的操作keys.forEach(key -> {// 獲取 key 的數據類型String type = jedis.type(key);System.out.println("Processing key: " + key + ",類型為:" + type);switch (type) {case "string":// 如果是字符串類型,直接調用 get 方法String strValue = jedis.get(key);System.out.println("String value: " + strValue);persistDataToDB(key, strValue);break;case "list":// 如果是列表類型,通過 lrange 獲取所有列表元素List<String> listValue = jedis.lrange(key, 0, -1);System.out.println("List value: " + listValue);break;case "set":// 如果是集合類型,通過 smembers 獲取所有成員Set<String> setValue = jedis.smembers(key);System.out.println("Set value: " + setValue);break;case "zset":// 如果是有序集合類型,通過 zrange 獲取所有元素(默認按分數從小到大排序)Set<String> zsetValue = jedis.zrange(key, 0, -1);System.out.println("ZSet value: " + zsetValue);break;case "hash":// 如果是 Hash 類型,通過 hgetAll 獲取所有鍵值對Map<String, String> hashValue = jedis.hgetAll(key);System.out.println("Hash value: " + hashValue);break;default:// 對于未知類型或其他類型的值,可以在這里處理System.out.println("Unknown type for key: " + key);break;}});// 可以適當休眠,避免對 Redis 服務器產生太大壓力try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("Interrupted: " + e.getMessage());}} while (!"0".equals(cursor)); // cursor 為 "0" 時表示遍歷結束jedis.close();System.out.println("數據遷移完成。");}/*** 模擬持久化數據到數據庫** @param key Redis 的鍵* @param value Redis 的值*/private static void persistDataToDB(String key, String value) {// 此處僅作模擬,可替換為真實的數據庫持久化操作System.out.println("持久化數據 - key: " + key + ", value: " + value);// 例如:// myDatabase.save(new DataEntity(key, value));}
}
總結
場景 | 推薦方案 | 避免方案 |
---|---|---|
生產環境遍歷海量鍵 | ?SCAN ?+ 合理COUNT ?值 | ?KEYS ?命令 |
精確統計或強一致性需求 | 維護索引集合/Lua 腳本 | 依賴SCAN ?結果 |
分頁查詢 | ?SCAN ?+MATCH ?+COUNT ? | 單次全量加載 |
最佳實踐:
- 優先使用
SCAN
? 替代KEYS
?,并在客戶端實現去重邏輯。 - 結合
TYPE
? 參數(Redis 6.0+)減少無效遍歷。
?