Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務(wù)key
1、問題背景
云服務(wù)運維工程師聯(lián)系我說老系統(tǒng)有個服務(wù)連接redis集群實例使用keys命令導(dǎo)致實例夯住了并給我截了個圖。然后剛開始我是挺懵逼的,同事跟我說在某個服務(wù)中,我去找了找壓根沒有,后來我仔細(xì)想了想,并看了運維老師提供的圖,我想到了方法找對應(yīng)的應(yīng)用進程。以下是排查及解決過程。
2、如何找到對應(yīng)的應(yīng)用進程
根據(jù)下面的圖,我們可以看到,redis集群的服務(wù)端口為9000,客戶端連接分配的客戶端本地通信端口【本地端口只是一個臨時標(biāo)識,用于客戶端與 Redis 之間的通信,通常是由操作系統(tǒng)在每次創(chuàng)建新連接時自動分配的,并不會影響連接的實際功能?!繛?9720,那么我們就可以通過netstat命令來查找對應(yīng)的應(yīng)用進程了。

2.1、使用netstat查找進程
進入應(yīng)用部署的服務(wù)器,使用如下netstat命令查找進程,如下圖,從下圖我們可以看出,進程是個java進程,進程號為15817
netstat -anlp |grep 9000 |grep EST |grep 39720

2.2、使用jps命令查看應(yīng)用名稱
使用jps命令查看java進程對應(yīng)的應(yīng)用名稱,通過命令我們可以看出
jps -l |grep 15817

3、問題代碼及原因分析
3.1、查找問題代碼
根據(jù)步驟2我們找到了對應(yīng)的應(yīng)用,下面我們就可以通過redis中的key關(guān)鍵詞YZ_MULTI_DIAG搜索代碼了,然后找到了如下圖的代碼,確實使用了keys命令。
private void cleanCache(String toUserId) {
Set<String> keys = stringRedisTemplate.keys("YZ_MULTI_DIAG:" + toUserId + "*");
stringRedisTemplate.delete(keys);
}
3.2、原因分析
keys 命令在 Redis 中遍歷所有的鍵,是一個阻塞操作,尤其是當(dāng) Redis 數(shù)據(jù)量大時,可能會導(dǎo)致 Redis 實例卡住或響應(yīng)變慢。在 Redis 中,keys 命令用于查找與給定模式匹配的所有鍵,它會掃描整個數(shù)據(jù)庫,并返回符合條件的所有鍵。這個命令在某些情況下會導(dǎo)致 Redis 實例“夯住”或變得非常緩慢,原因如下:
3.2.1、 阻塞和性能影響
keys命令需要遍歷 Redis 實例中所有的鍵,無論數(shù)據(jù)庫中有多少個鍵。對于存儲大量鍵的 Redis 實例來說,keys命令會消耗大量的 CPU 和內(nèi)存資源,因為它必須檢查每個鍵,并將結(jié)果返回給客戶端。- 如果有大量的鍵,
keys命令可能會導(dǎo)致 Redis 被阻塞,直到命令完成執(zhí)行。在此期間,Redis 無法處理其他客戶端請求,這可能會導(dǎo)致延遲或服務(wù)中斷。
3.2.2、 不適合生產(chǎn)環(huán)境
- 在生產(chǎn)環(huán)境中,通常不建議使用
keys命令,特別是在有大量鍵值對的情況下。keys命令的性能是 O(N),其中 N 是數(shù)據(jù)庫中鍵的數(shù)量。這意味著數(shù)據(jù)庫中鍵越多,執(zhí)行時間就越長,負(fù)載越重。 - 更適合使用
scan命令,它是增量式的,并不會一次性返回所有匹配的鍵,而是通過多次迭代逐步獲取。這使得 Redis 在掃描鍵時不會被完全阻塞。
3.2.3、 其他客戶端請求的影響
- 由于
keys命令會導(dǎo)致 Redis 掃描整個鍵空間,它會占用 Redis 實例的 CPU 和內(nèi)存資源,這可能導(dǎo)致其他客戶端請求的響應(yīng)時間延遲,甚至阻塞其他操作,導(dǎo)致整個 Redis 實例性能下降。 - 在 Redis 集群環(huán)境中,
keys命令會對集群的每個節(jié)點進行全局掃描,可能會對整個集群的性能產(chǎn)生影響。
4、優(yōu)化方案
- 使用
scan命令替代keys命令。scan命令是增量的,可以分批次掃描鍵,避免一次性操作導(dǎo)致的阻塞。 - 如果需要列出鍵,盡量使用特定的鍵模式(例如,前綴)來限制掃描的范圍,避免掃描整個數(shù)據(jù)庫。
- 在生產(chǎn)環(huán)境中,應(yīng)該避免在高負(fù)載期間使用
keys命令。
優(yōu)化后的代碼如下,使用類似分頁概念進行批量刪除。
private void cleanCache(String toUserId) {
String pattern = "YZ_MULTI_DIAG:" + toUserId + "*";
ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(100).build();
stringRedisTemplate.execute((RedisCallback<Void>) connection -> {
String cursor = "0"; // 初始游標(biāo)
try {
do {
// 使用SCAN命令分頁獲取匹配的鍵
Cursor<byte[]> scanCursor = connection.scan(scanOptions);
List<byte[]> keysToDelete = new ArrayList<>();
while (scanCursor.hasNext()) {
keysToDelete.add(scanCursor.next());
// 分批刪除,避免內(nèi)存占用過高
if (keysToDelete.size() >= 100) {
connection.del(keysToDelete.toArray(new byte[0][]));
keysToDelete.clear();
}
}
// 刪除剩余的鍵
if (!keysToDelete.isEmpty()) {
connection.del(keysToDelete.toArray(new byte[0][]));
}
cursor = scanCursor.getCursorId() + ""; // 更新游標(biāo)
} while (!"0".equals(cursor)); // 如果游標(biāo)為0,表示掃描結(jié)束
} catch (Exception e) {
log.error("Error while scanning and deleting Redis keys with pattern: {}", pattern, e);
}
return null;
});
}
5、測試驗證
5.1、編寫測試類
新增測試類,代碼如下,新增100個key,然后按照每個批次10個進行刪除測試,代碼如下
package com.jianjang.zhgl.person.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @program: zhgl_server
* @description: 緩存清理測試類
* @author: Jian Jang
* @create: 2025-05-06 11:25:51
* @blame ZHSF Team
*/
@Slf4j
@ActiveProfiles("local")
@SpringBootTest
public class RedisCleanCacheTest {
/**
* 測試key
*/
private final static String TEST_KEY = "TEST_KEY:";
private final static String BIZ_KEY = "userId";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void addCache() {
for (int i = 0; i < 100; i++) {
stringRedisTemplate.opsForValue().set(TEST_KEY+BIZ_KEY+i, "value" + i);
}
}
@Test
public void cleanCache() {
cleanCache(BIZ_KEY, 10);
}
/**
* 清除緩存內(nèi)容
*
* @param redisKey
* @param batchSize
*/
private void cleanCache(String redisKey, int batchSize) {
String pattern = TEST_KEY + redisKey + "*";
ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(batchSize).build();
stringRedisTemplate.execute((RedisCallback<Void>) connection -> {
String cursor = "0"; // 初始游標(biāo)
try {
do {
// 使用SCAN命令分頁獲取匹配的鍵
Cursor<byte[]> scanCursor = connection.scan(scanOptions);
List<byte[]> keysToDelete = new ArrayList<>();
while (scanCursor.hasNext()) {
keysToDelete.add(scanCursor.next());
// 分批刪除,避免內(nèi)存占用過高
if (keysToDelete.size() >= batchSize) {
connection.del(keysToDelete.toArray(new byte[0][]));
keysToDelete.clear();
}
}
// 刪除剩余的鍵
if (!keysToDelete.isEmpty()) {
connection.del(keysToDelete.toArray(new byte[0][]));
}
cursor = scanCursor.getCursorId() + ""; // 更新游標(biāo)
} while (!"0".equals(cursor)); // 如果游標(biāo)為0,表示掃描結(jié)束
} catch (Exception e) {
log.error("Error while scanning and deleting Redis keys with pattern: {}", pattern, e);
}
return null;
});
}
}
5.2、測試新增
執(zhí)行新增測試方法后,新增成功,如下圖,

5.3、測試批量刪除
執(zhí)行批量刪除方法后,刪除成功,如下圖,100個TEST_KEY已被清除。

到此這篇關(guān)于Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務(wù)key的文章就介紹到這了,更多相關(guān)Springboot清除Redis業(yè)務(wù)key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?ThreadPoolExecutor線程池有關(guān)介紹
這篇文章主要介紹了Java?ThreadPoolExecutor線程池有關(guān)介紹,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09
Eclipse中導(dǎo)入Maven Web項目并配置其在Tomcat中運行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項目并配置其在Tomcat中運行圖文詳解,需要的朋友可以參考下2017-12-12
Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用實例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用,以實例形式較為詳細(xì)的分析了java網(wǎng)絡(luò)編程的原理與服務(wù)器端客戶端的實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-04-04
Java數(shù)據(jù)結(jié)構(gòu)之對象的比較
比較對象是面向?qū)ο缶幊陶Z言的一個基本特征,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之對象的比較,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02
springboot如何使用logback-spring配置日志格式,并分環(huán)境配置
這篇文章主要介紹了springboot如何使用logback-spring配置日志格式,并分環(huán)境配置的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關(guān)與請求過濾
這篇文章主要給大家介紹了關(guān)于Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關(guān)與請求過濾的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05
Java中如何將?int[]?數(shù)組轉(zhuǎn)換為?ArrayList(list)
這篇文章主要介紹了Java中將?int[]?數(shù)組?轉(zhuǎn)換為?List(ArrayList),本文通過示例代碼給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12

