Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務(wù)key
1、問題背景
云服務(wù)運維工程師聯(lián)系我說老系統(tǒng)有個服務(wù)連接redis集群實例使用keys命令導(dǎo)致實例夯住了并給我截了個圖。然后剛開始我是挺懵逼的,同事跟我說在某個服務(wù)中,我去找了找壓根沒有,后來我仔細想了想,并看了運維老師提供的圖,我想到了方法找對應(yīng)的應(yīng)用進程。以下是排查及解決過程。
2、如何找到對應(yīng)的應(yīng)用進程
根據(jù)下面的圖,我們可以看到,redis集群的服務(wù)端口為9000,客戶端連接分配的客戶端本地通信端口【本地端口只是一個臨時標(biāo)識,用于客戶端與 Redis 之間的通信,通常是由操作系統(tǒng)在每次創(chuàng)建新連接時自動分配的,并不會影響連接的實際功能。】為39720,那么我們就可以通過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í)行時間就越長,負載越重。 - 更適合使用
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)該避免在高負載期間使用
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)介紹,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09Eclipse中導(dǎo)入Maven Web項目并配置其在Tomcat中運行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項目并配置其在Tomcat中運行圖文詳解,需要的朋友可以參考下2017-12-12Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用實例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用,以實例形式較為詳細的分析了java網(wǎng)絡(luò)編程的原理與服務(wù)器端客戶端的實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-04-04Java數(shù)據(jù)結(jié)構(gòu)之對象的比較
比較對象是面向?qū)ο缶幊陶Z言的一個基本特征,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之對象的比較,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-02-02springboot如何使用logback-spring配置日志格式,并分環(huán)境配置
這篇文章主要介紹了springboot如何使用logback-spring配置日志格式,并分環(huán)境配置的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關(guān)與請求過濾
這篇文章主要給大家介紹了關(guān)于Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關(guān)與請求過濾的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05Java中如何將?int[]?數(shù)組轉(zhuǎn)換為?ArrayList(list)
這篇文章主要介紹了Java中將?int[]?數(shù)組?轉(zhuǎn)換為?List(ArrayList),本文通過示例代碼給大家講解的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12