Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務key
1、問題背景
云服務運維工程師聯(lián)系我說老系統(tǒng)有個服務連接redis集群實例使用keys命令導致實例夯住了并給我截了個圖。然后剛開始我是挺懵逼的,同事跟我說在某個服務中,我去找了找壓根沒有,后來我仔細想了想,并看了運維老師提供的圖,我想到了方法找對應的應用進程。以下是排查及解決過程。
2、如何找到對應的應用進程
根據(jù)下面的圖,我們可以看到,redis集群的服務端口為9000,客戶端連接分配的客戶端本地通信端口【本地端口只是一個臨時標識,用于客戶端與 Redis 之間的通信,通常是由操作系統(tǒng)在每次創(chuàng)建新連接時自動分配的,并不會影響連接的實際功能?!繛?9720,那么我們就可以通過netstat命令來查找對應的應用進程了。
2.1、使用netstat查找進程
進入應用部署的服務器,使用如下netstat命令查找進程,如下圖,從下圖我們可以看出,進程是個java進程,進程號為15817
netstat -anlp |grep 9000 |grep EST |grep 39720
2.2、使用jps命令查看應用名稱
使用jps命令查看java進程對應的應用名稱,通過命令我們可以看出
jps -l |grep 15817
3、問題代碼及原因分析
3.1、查找問題代碼
根據(jù)步驟2我們找到了對應的應用,下面我們就可以通過redis中的key關鍵詞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 中遍歷所有的鍵,是一個阻塞操作,尤其是當 Redis 數(shù)據(jù)量大時,可能會導致 Redis 實例卡住或響應變慢。在 Redis 中,keys
命令用于查找與給定模式匹配的所有鍵,它會掃描整個數(shù)據(jù)庫,并返回符合條件的所有鍵。這個命令在某些情況下會導致 Redis 實例“夯住”或變得非常緩慢,原因如下:
3.2.1、 阻塞和性能影響
keys
命令需要遍歷 Redis 實例中所有的鍵,無論數(shù)據(jù)庫中有多少個鍵。對于存儲大量鍵的 Redis 實例來說,keys
命令會消耗大量的 CPU 和內(nèi)存資源,因為它必須檢查每個鍵,并將結(jié)果返回給客戶端。- 如果有大量的鍵,
keys
命令可能會導致 Redis 被阻塞,直到命令完成執(zhí)行。在此期間,Redis 無法處理其他客戶端請求,這可能會導致延遲或服務中斷。
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
命令會導致 Redis 掃描整個鍵空間,它會占用 Redis 實例的 CPU 和內(nèi)存資源,這可能導致其他客戶端請求的響應時間延遲,甚至阻塞其他操作,導致整個 Redis 實例性能下降。 - 在 Redis 集群環(huán)境中,
keys
命令會對集群的每個節(jié)點進行全局掃描,可能會對整個集群的性能產(chǎn)生影響。
4、優(yōu)化方案
- 使用
scan
命令替代keys
命令。scan
命令是增量的,可以分批次掃描鍵,避免一次性操作導致的阻塞。 - 如果需要列出鍵,盡量使用特定的鍵模式(例如,前綴)來限制掃描的范圍,避免掃描整個數(shù)據(jù)庫。
- 在生產(chǎn)環(huán)境中,應該避免在高負載期間使用
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"; // 初始游標 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() + ""; // 更新游標 } while (!"0".equals(cursor)); // 如果游標為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"; // 初始游標 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() + ""; // 更新游標 } while (!"0".equals(cursor)); // 如果游標為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已被清除。
到此這篇關于Springboot如何優(yōu)雅高效的清除Redis中的業(yè)務key的文章就介紹到這了,更多相關Springboot清除Redis業(yè)務key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java?ThreadPoolExecutor線程池有關介紹
這篇文章主要介紹了Java?ThreadPoolExecutor線程池有關介紹,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09Eclipse中導入Maven Web項目并配置其在Tomcat中運行圖文詳解
這篇文章主要介紹了Eclipse中導入Maven Web項目并配置其在Tomcat中運行圖文詳解,需要的朋友可以參考下2017-12-12springboot如何使用logback-spring配置日志格式,并分環(huán)境配置
這篇文章主要介紹了springboot如何使用logback-spring配置日志格式,并分環(huán)境配置的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關與請求過濾
這篇文章主要給大家介紹了關于Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關與請求過濾的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-05-05Java中如何將?int[]?數(shù)組轉(zhuǎn)換為?ArrayList(list)
這篇文章主要介紹了Java中將?int[]?數(shù)組?轉(zhuǎn)換為?List(ArrayList),本文通過示例代碼給大家講解的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12