# 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