Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務(wù)key
1、問題背景
云服務(wù)運(yùn)維工程師聯(lián)系我說老系統(tǒng)有個(gè)服務(wù)連接redis集群實(shí)例使用keys命令導(dǎo)致實(shí)例夯住了并給我截了個(gè)圖。然后剛開始我是挺懵逼的,同事跟我說在某個(gè)服務(wù)中,我去找了找壓根沒有,后來我仔細(xì)想了想,并看了運(yùn)維老師提供的圖,我想到了方法找對應(yīng)的應(yīng)用進(jìn)程。以下是排查及解決過程。
2、如何找到對應(yīng)的應(yīng)用進(jìn)程
根據(jù)下面的圖,我們可以看到,redis集群的服務(wù)端口為9000,客戶端連接分配的客戶端本地通信端口【本地端口只是一個(gè)臨時(shí)標(biāo)識,用于客戶端與 Redis 之間的通信,通常是由操作系統(tǒng)在每次創(chuàng)建新連接時(shí)自動(dòng)分配的,并不會(huì)影響連接的實(shí)際功能?!繛?9720,那么我們就可以通過netstat命令來查找對應(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)程號為15817
netstat -anlp |grep 9000 |grep EST |grep 39720
2.2、使用jps命令查看應(yīng)用名稱
使用jps命令查看java進(jìn)程對應(yīng)的應(yīng)用名稱,通過命令我們可以看出
jps -l |grep 15817
3、問題代碼及原因分析
3.1、查找問題代碼
根據(jù)步驟2我們找到了對應(yīng)的應(yīng)用,下面我們就可以通過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ù)庫,并返回符合條件的所有鍵。這個(gè)命令在某些情況下會(huì)導(dǎo)致 Redis 實(shí)例“夯住”或變得非常緩慢,原因如下:
3.2.1、 阻塞和性能影響
keys
命令需要遍歷 Redis 實(shí)例中所有的鍵,無論數(shù)據(jù)庫中有多少個(gè)鍵。對于存儲(chǔ)大量鍵的 Redis 實(shí)例來說,keys
命令會(huì)消耗大量的 CPU 和內(nèi)存資源,因?yàn)樗仨殭z查每個(gè)鍵,并將結(jié)果返回給客戶端。- 如果有大量的鍵,
keys
命令可能會(huì)導(dǎo)致 Redis 被阻塞,直到命令完成執(zhí)行。在此期間,Redis 無法處理其他客戶端請求,這可能會(huì)導(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í)行時(shí)間就越長,負(fù)載越重。 - 更適合使用
scan
命令,它是增量式的,并不會(huì)一次性返回所有匹配的鍵,而是通過多次迭代逐步獲取。這使得 Redis 在掃描鍵時(shí)不會(huì)被完全阻塞。
3.2.3、 其他客戶端請求的影響
- 由于
keys
命令會(huì)導(dǎo)致 Redis 掃描整個(gè)鍵空間,它會(huì)占用 Redis 實(shí)例的 CPU 和內(nèi)存資源,這可能導(dǎo)致其他客戶端請求的響應(yīng)時(shí)間延遲,甚至阻塞其他操作,導(dǎo)致整個(gè) Redis 實(shí)例性能下降。 - 在 Redis 集群環(huán)境中,
keys
命令會(huì)對集群的每個(gè)節(jié)點(diǎn)進(jìn)行全局掃描,可能會(huì)對整個(gè)集群的性能產(chǎn)生影響。
4、優(yōu)化方案
- 使用
scan
命令替代keys
命令。scan
命令是增量的,可以分批次掃描鍵,避免一次性操作導(dǎo)致的阻塞。 - 如果需要列出鍵,盡量使用特定的鍵模式(例如,前綴)來限制掃描的范圍,避免掃描整個(gè)數(shù)據(jù)庫。
- 在生產(chǎn)環(huán)境中,應(yīng)該避免在高負(fù)載期間使用
keys
命令。
優(yōu)化后的代碼如下,使用類似分頁概念進(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命令分頁獲取匹配的鍵 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、測試驗(yàn)證
5.1、編寫測試類
新增測試類,代碼如下,新增100個(gè)key,然后按照每個(gè)批次10個(gè)進(jìn)行刪除測試,代碼如下
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個(gè)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)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解,需要的朋友可以參考下2017-12-12Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用實(shí)例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用,以實(shí)例形式較為詳細(xì)的分析了java網(wǎng)絡(luò)編程的原理與服務(wù)器端客戶端的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04十五道tomcat面試題,為數(shù)不多的機(jī)會(huì)!
這篇文章主要介紹了十五道tomcat面試題,Tomcat的本質(zhì)是一個(gè)Servlet容器。一個(gè)Servlet能做的事情是:處理請求資源,并為客戶端填充response對象,需要的朋友可以參考下2021-08-08Java數(shù)據(jù)結(jié)構(gòu)之對象的比較
比較對象是面向?qū)ο缶幊陶Z言的一個(gè)基本特征,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之對象的比較,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02springboot如何使用logback-spring配置日志格式,并分環(huán)境配置
這篇文章主要介紹了springboot如何使用logback-spring配置日志格式,并分環(huán)境配置的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Cloud入門教程之Zuul實(shí)現(xiàn)API網(wǎng)關(guān)與請求過濾
這篇文章主要給大家介紹了關(guān)于Spring Cloud入門教程之Zuul實(shí)現(xiàn)API網(wǎng)關(guān)與請求過濾的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05Java中如何將?int[]?數(shù)組轉(zhuǎn)換為?ArrayList(list)
這篇文章主要介紹了Java中將?int[]?數(shù)組?轉(zhuǎn)換為?List(ArrayList),本文通過示例代碼給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12