# Spring Boot Redis:`SCAN` vs `KEYS` 筆記 --- ## 一、`KEYS` 與 `SCAN` 差異 | 指令 | 用途 | 特性 | 適用情境 | | ---- | ----------------------- | ----------------- | ------------ | | KEYS | 一次性搜尋所有符合 pattern 的 key | 一次性返回所有 key,阻塞效能差 | 僅適合開發測試、小量資料 | | SCAN | 漸進式分批搜尋匹配 key | 分批返回、非阻塞、效能佳 | 生產環境、大量資料 | --- ## 二、指令詳細說明與範例 ### 1. KEYS * **指令格式**:`KEYS pattern` * **特性**:一次全部撈出,阻塞主執行緒,key 很多會讓 Redis 卡死。 * **範例**: ```shell KEYS user:* ``` 會回傳所有以 `user:` 開頭的 key(哪怕有百萬筆)。 * **Spring Boot 實作**: ```java Set<String> keys = redisTemplate.keys("user:*"); for (String key : keys) { // 處理每個 key } ``` --- ### 2. SCAN * **指令格式**:`SCAN cursor [MATCH pattern] [COUNT n]` * **特性**:分批回傳、非阻塞、每次一小批,對 Redis 伺服器影響小。 * **範例**: ```shell SCAN 0 MATCH user:* COUNT 1000 ``` 分批回傳符合 `user:*` 的 key,每批最多 1000 筆,直到游標回到 0 全部遍歷完。 * **Spring Boot 實作**: ```java ScanOptions options = ScanOptions.scanOptions().match("user:*").count(1000).build(); Cursor<byte[]> cursor = redisTemplate.execute( (RedisConnection connection) -> connection.scan(options)); while (cursor.hasNext()) { String key = new String(cursor.next()); // 處理 key } ``` --- ## 三、效能分析與實務建議 ### 1. KEYS 的效能與風險 * **會阻塞主執行緒**:大量 key 時會讓 Redis 無法處理其他請求,嚴重可能造成服務宕機。 * **僅適合**:開發、測試、key 很少的情境。 ### 2. SCAN 的效能與安全性 * **分批處理**,不會阻塞主執行緒,對外服務無感。 * 可調整單批數量(COUNT),兼顧效能與穩定性。 * 生產環境唯一推薦用法。 ### 3. 實際效能對比說明 * KEYS 雖然「看似」總花費時間短,但這是「犧牲 Redis 服務存活」換來的。 * SCAN 可多執行緒、調大 COUNT,總耗時與 KEYS 相近,但不影響其他業務請求。 * **總結**:生產永遠用 SCAN,KEYS 只用在安全無壓力的小資料測試。 --- ## 四、補充:SCAN 常見實作優化 * 可調整 `COUNT` 參數(如 1000、5000、10000)看主機負載調整效能。 * 支援多協程分批併發 scan(自行合併結果去重),可進 --- ## 五、Note 最近在做的專案,使用一個 spring boot 專案作為將 Oracle 資料 ETL 到 Redis 的工具, 當執行 ETL 時,會把 redis 中的某一個 status key 設定成 `正在執行ETL`,ETL 完成後再把 status key 設定成 `ETL完成`。 本來我是覺得這樣就可以避開我正在執行的時候,其他服務要來取我 Redis 的資料。 所以我就可以放心的使用 KEYS 指令來取出 Redis 中的資料, 但我現在想一想,Redis 只有一條執行緒,那我正在使用 keys 的時候,別人好像也無法來查我的 status,這樣其他服務要來看 Redis status 的時候,好像就還是會因為只有一條執行緒,而被卡住,導致 TIme out? 目前覺得可能要改成 scan + count + 去重 + pipeline + 多執行緒 這樣或許會比較安全,又可以提升效能 範例 ```java import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import java.util.*; import java.util.concurrent.*; public class RedisBatchDeleteTask { private final RedisTemplate<String, String> redisTemplate; private final List<String> prefixes; private final int batchSize; private final int threadCount; // 用於跨執行緒去重 key private final Set<String> dedupSet = ConcurrentHashMap.newKeySet(); public RedisBatchDeleteTask(RedisTemplate<String, String> redisTemplate, List<String> prefixes, int batchSize, int threadCount) { this.redisTemplate = redisTemplate; this.prefixes = prefixes; this.batchSize = batchSize; this.threadCount = threadCount; } public void executeBatchDelete() throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(threadCount); List<Future<?>> futures = new ArrayList<>(); for (String prefix : prefixes) { futures.add(pool.submit(() -> { ScanOptions options = ScanOptions.scanOptions().match(prefix).count(batchSize).build(); Cursor<byte[]> cursor = redisTemplate.execute( (RedisConnection conn) -> conn.scan(options) ); List<String> batch = new ArrayList<>(); try { while (cursor != null && cursor.hasNext()) { String key = new String(cursor.next()); // 去重邏輯 if (dedupSet.add(key)) { batch.add(key); } if (batch.size() >= batchSize) { batchDelete(batch); batch.clear(); } } // 處理最後一批 if (!batch.isEmpty()) { batchDelete(batch); } } finally { if (cursor != null) cursor.close(); } })); } for (Future<?> f : futures) f.get(); // 等待所有 thread 結束 pool.shutdown(); } // Pipeline 批次刪除 private void batchDelete(List<String> keys) { redisTemplate.executePipelined((RedisCallback<Object>) conn -> { for (String k : keys) { conn.keyCommands().del(k.getBytes()); } return null; }); } public static void main(String[] args) throws InterruptedException { // 假設你已經有一個 bean: redisTemplate RedisTemplate<String, String> redisTemplate = ...; List<String> prefixes = Arrays.asList("user:a*", "user:b*", "user:c*", "user:d*"); int batchSize = 2000; int threadCount = prefixes.size(); RedisBatchDeleteTask task = new RedisBatchDeleteTask(redisTemplate, prefixes, batchSize, threadCount); task.executeBatchDelete(); System.out.println("批次刪除完成!實際刪除 key 數量: " + task.dedupSet.size()); } } ``` --- ###### tags: redis, spring-boot, scan, keys, performance