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

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

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

3、問(wèn)題代碼及原因分析
3.1、查找問(wèn)題代碼
根據(jù)步驟2我們找到了對(duì)應(yīng)的應(yīng)用,下面我們就可以通過(guò)redis中的key關(guān)鍵詞YZ_MULTI_DIAG搜索代碼了,然后找到了如下圖的代碼,確實(shí)使用了keys命令。
private void cleanCache(String toUserId) {
Set<String> keys = stringRedisTemplate.keys("YZ_MULTI_DIAG:" + toUserId + "*");
stringRedisTemplate.delete(keys);
}
3.2、原因分析
keys 命令在 Redis 中遍歷所有的鍵,是一個(gè)阻塞操作,尤其是當(dāng) Redis 數(shù)據(jù)量大時(shí),可能會(huì)導(dǎo)致 Redis 實(shí)例卡住或響應(yīng)變慢。在 Redis 中,keys 命令用于查找與給定模式匹配的所有鍵,它會(huì)掃描整個(gè)數(shù)據(jù)庫(kù),并返回符合條件的所有鍵。這個(gè)命令在某些情況下會(huì)導(dǎo)致 Redis 實(shí)例“夯住”或變得非常緩慢,原因如下:
3.2.1、 阻塞和性能影響
keys命令需要遍歷 Redis 實(shí)例中所有的鍵,無(wú)論數(shù)據(jù)庫(kù)中有多少個(gè)鍵。對(duì)于存儲(chǔ)大量鍵的 Redis 實(shí)例來(lái)說(shuō),keys命令會(huì)消耗大量的 CPU 和內(nèi)存資源,因?yàn)樗仨殭z查每個(gè)鍵,并將結(jié)果返回給客戶端。- 如果有大量的鍵,
keys命令可能會(huì)導(dǎo)致 Redis 被阻塞,直到命令完成執(zhí)行。在此期間,Redis 無(wú)法處理其他客戶端請(qǐng)求,這可能會(huì)導(dǎo)致延遲或服務(wù)中斷。
3.2.2、 不適合生產(chǎn)環(huán)境
- 在生產(chǎn)環(huán)境中,通常不建議使用
keys命令,特別是在有大量鍵值對(duì)的情況下。keys命令的性能是 O(N),其中 N 是數(shù)據(jù)庫(kù)中鍵的數(shù)量。這意味著數(shù)據(jù)庫(kù)中鍵越多,執(zhí)行時(shí)間就越長(zhǎng),負(fù)載越重。 - 更適合使用
scan命令,它是增量式的,并不會(huì)一次性返回所有匹配的鍵,而是通過(guò)多次迭代逐步獲取。這使得 Redis 在掃描鍵時(shí)不會(huì)被完全阻塞。
3.2.3、 其他客戶端請(qǐng)求的影響
- 由于
keys命令會(huì)導(dǎo)致 Redis 掃描整個(gè)鍵空間,它會(huì)占用 Redis 實(shí)例的 CPU 和內(nèi)存資源,這可能導(dǎo)致其他客戶端請(qǐng)求的響應(yīng)時(shí)間延遲,甚至阻塞其他操作,導(dǎo)致整個(gè) Redis 實(shí)例性能下降。 - 在 Redis 集群環(huán)境中,
keys命令會(huì)對(duì)集群的每個(gè)節(jié)點(diǎn)進(jìn)行全局掃描,可能會(huì)對(duì)整個(gè)集群的性能產(chǎn)生影響。
4、優(yōu)化方案
- 使用
scan命令替代keys命令。scan命令是增量的,可以分批次掃描鍵,避免一次性操作導(dǎo)致的阻塞。 - 如果需要列出鍵,盡量使用特定的鍵模式(例如,前綴)來(lái)限制掃描的范圍,避免掃描整個(gè)數(shù)據(jù)庫(kù)。
- 在生產(chǎn)環(huán)境中,應(yīng)該避免在高負(fù)載期間使用
keys命令。
優(yōu)化后的代碼如下,使用類似分頁(yè)概念進(jìn)行批量刪除。
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命令分頁(yè)獲取匹配的鍵
Cursor<byte[]> scanCursor = connection.scan(scanOptions);
List<byte[]> keysToDelete = new ArrayList<>();
while (scanCursor.hasNext()) {
keysToDelete.add(scanCursor.next());
// 分批刪除,避免內(nèi)存占用過(guò)高
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、測(cè)試驗(yàn)證
5.1、編寫(xiě)測(cè)試類
新增測(cè)試類,代碼如下,新增100個(gè)key,然后按照每個(gè)批次10個(gè)進(jìn)行刪除測(cè)試,代碼如下
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: 緩存清理測(cè)試類
* @author: Jian Jang
* @create: 2025-05-06 11:25:51
* @blame ZHSF Team
*/
@Slf4j
@ActiveProfiles("local")
@SpringBootTest
public class RedisCleanCacheTest {
/**
* 測(cè)試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命令分頁(yè)獲取匹配的鍵
Cursor<byte[]> scanCursor = connection.scan(scanOptions);
List<byte[]> keysToDelete = new ArrayList<>();
while (scanCursor.hasNext()) {
keysToDelete.add(scanCursor.next());
// 分批刪除,避免內(nèi)存占用過(guò)高
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、測(cè)試新增
執(zhí)行新增測(cè)試方法后,新增成功,如下圖,

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

到此這篇關(guān)于Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務(wù)key的文章就介紹到這了,更多相關(guān)Springboot清除Redis業(yè)務(wù)key內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot開(kāi)啟Swagger并配置基本信息方式
這篇文章主要介紹了SpringBoot開(kāi)啟Swagger并配置基本信息方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
MySQL中的樂(lè)觀鎖和悲觀鎖入門(mén)簡(jiǎn)介
文章介紹了MySQL中的樂(lè)觀鎖和悲觀鎖兩種并發(fā)控制策略,悲觀鎖在每次訪問(wèn)數(shù)據(jù)時(shí)加鎖,適用于高沖突場(chǎng)景,樂(lè)觀鎖通過(guò)版本控制在更新時(shí)檢查數(shù)據(jù)是否被修改,適用于低沖突場(chǎng)景,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2026-01-01
Spring Boot集成Spring Cloud Security進(jìn)行安全增強(qiáng)的方法
Spring Cloud Security是Spring Security的擴(kuò)展,它提供了對(duì)Spring Cloud體系中的服務(wù)認(rèn)證和授權(quán)的支持,包括OAuth2、JWT等,這篇文章主要介紹了Spring Boot集成Spring Cloud Security進(jìn)行安全增強(qiáng),需要的朋友可以參考下2024-11-11
springmvc請(qǐng)求參數(shù)的綁定的項(xiàng)目實(shí)踐
本文主要介紹了springmvc請(qǐng)求參數(shù)的綁定的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-11-11
RestTemplate接口調(diào)用神器常見(jiàn)用法匯總
這篇文章主要介紹了RestTemplate接口調(diào)用神器常見(jiàn)用法匯總,通過(guò)案例代碼詳細(xì)介紹RestTemplate接口調(diào)用神器常見(jiàn)用法,需要的朋友可以參考下2022-07-07

