哎,最近阿粉又雙叒叕犯事了。git
事情是這樣的,前一段時間阿粉公司生產交易偶發報錯,一番排查下來最終緣由是由於 Redis 命令執行超時。github
但是使人不解的是,生產交易僅僅使用 Redis set 這個簡單命令,這個命令講道理是不可能會執行這麼慢。web
那究竟是什麼致使這個問題那?面試
爲了找出這個問題,咱們查看分析了一下 Redis 最近的慢日誌,最終發現耗時比較多命令爲 keys XX*
redis
看到這個命令操做的鍵的前綴,阿粉才發現這是本身負責的應用。但是阿粉排查一下,雖然本身的代碼並無主動去使用 keys
命令,可是底層使用框架卻在間接使用,因而就有了今天這個問題。spring
問題緣由
阿粉負責的應用是一個管理後臺應用,權限管理使用 Shiro 框架,因爲存在多個節點,須要使用分佈式 Session,因而這裏使用 Redis 存儲 Session 信息。數據庫
❝畫外音:不知道分佈式 Session ,能夠看看阿粉以前寫的 一口氣說出 4 種分佈式一致性 Session 實現方式,面試槓槓的~數組
❞
因爲 Shiro 並無直接提供 Redis 存儲 Session 組件,阿粉不得不使用 Github 一個開源組件 shiro-redis。微信
因爲 Shiro 框架須要按期驗證 Session 是否有效,因而 Shiro 底層將會調用 SessionDAO#getActiveSessions
獲取全部的 Session 信息。app
而 shiro-redis
正好繼承 SessionDAO
這個接口,底層使用用keys
命令查找 Redis 全部存儲的 Session
key。
public Set<byte[]> keys(byte[] pattern){
checkAndInit();
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = jedis.keys(pattern);
}finally{
jedis.close();
}
return keys;
}
找到問題緣由,解決辦法就比較簡單了,github 上查找到解決方案,升級一下 shiro-redis
到最新版本。
在這個版本,shiro-redis
採用 scan
命令代替 keys
,從而修復這個問題。
public Set<byte[]> keys(byte[] pattern) {
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = new HashSet<byte[]>();
ScanParams params = new ScanParams();
params.count(count);
params.match(pattern);
byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
ScanResult<byte[]> scanResult;
do{
scanResult = jedis.scan(cursor,params);
keys.addAll(scanResult.getResult());
cursor = scanResult.getCursorAsBytes();
}while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0);
}finally{
jedis.close();
}
return keys;
}
雖然問題成功解決了,可是阿粉內心仍是有點不解。
爲何keys
指令會致使其餘命令執行變慢?
爲何Keys
指令查詢會這麼慢?
爲何Scan
指令就沒有問題?
Redis 執行命令的原理
首先咱們來看第一個問題,爲何keys
指令會致使其餘命令執行變慢?
回答這個問題,咱們首先看下 Redis 客戶端執行一條命令的狀況:
站在客戶端的視角,執行一條命令分爲三步:
-
發送命令 -
執行命令 -
返回結果
可是這僅僅客戶端本身覺得的過程,可是實際上同一時刻,可能存在不少客戶端發送命令給 Redis,而 Redis 咱們都知道它採用的是單線程模型。
爲了處理同一時刻全部的客戶端的請求命令,Redis 內部採用了隊列的方式,排隊執行。
因而客戶端執行一條命令實際須要四步:
-
發送命令 -
命令排隊 -
執行命令 -
返回結果
因爲 Redis 單線程執行命令,只能順序從隊列取出任務開始執行。
只要 3 這個過程執行命令速度過慢,隊列其餘任務不得不進行等待,這對外部客戶端看來,Redis 好像就被阻塞同樣,一直得不到響應。
因此使用 Redis 過程切勿執行須要長時間運行的指令,這樣可能致使 Redis 阻塞,影響執行其餘指令。
KEYS 原理
接下來開始回答第二個問題,爲何Keys
指令查詢會這麼慢?
回答這個問題以前,請你們回想一下 Redis 底層存儲結構。
不太清楚朋友的也不要緊,你們能夠回看一下阿粉以前的文章「阿里面試官:HashMap 熟悉吧?好的,那就來聊聊 Redis 字典吧!」。
這裏阿粉複製以前文章內容,Redis 底層使用字典這種結構,這個結構與 Java HashMap 底層比較相似。
keys
命令須要返回全部的符合給定模式 pattern
的 Redis 中鍵,爲了實現這個目的,Redis 不得不遍歷字典中 ht[0]
哈希表底層數組,這個時間複雜度爲 「O(N)」(N 爲 Redis 中 key 全部的數量)。
若是 Redis 中 key 的數量不多,那麼這個執行速度仍是也會很快。等到 Redis key 的數量慢慢更加,上升到百萬、千萬、甚至上億級別,那這個執行速度就會很慢很慢。
下面是阿粉本地作的一次實驗,使用 lua 腳本往 Redis 中增長 10W 個 key,而後使用 keys
查詢全部鍵,這個查詢大概會阻塞十幾秒的時間。
eval "for i=1,100000 do redis.call('set',i,i+1) end" 0
❝這裏阿粉使用 Docker 部署 Redis,性能可能會稍差。
❞
SCAN 原理
最後咱們來看下第三個問題,爲何scan
指令就沒有問題?
這是由於 scan
命令採用一種黑科技-「基於遊標的迭代器」。
每次調用 scan
命令,Redis 都會向用戶返回一個新的遊標以及必定數量的 key。下次再想繼續獲取剩餘的 key,須要將這個遊標傳入 scan 命令, 以此來延續以前的迭代過程。
簡單來說,scan
命令使用分頁查詢 redis 。
下面是一個 scan 命令的迭代過程示例:
scan
命令使用遊標這種方式,巧妙將一次全量查詢拆分紅屢次,下降查詢複雜度。
雖然 scan
命令時間複雜度與 keys
同樣,都是 「O(N)」,可是因爲 scan
命令只須要返回少許的 key,因此執行速度會很快。
最後,雖然scan
命令解決 keys
不足,可是同時也引入其餘一些缺陷:
-
同一個元素可能會被返回屢次,這就須要咱們應用程序增長處理重複元素功能。 -
若是一個元素在迭代過程增長到 redis,或者說在迭代過程被刪除,那個這個元素會被返回,也可能不會。
以上這些缺陷,在咱們開發中須要考慮這種狀況。
除了 scan
之外,redis 還有其餘幾個用於增量迭代命令:
-
sscan
:用於迭代當前數據庫中的數據庫鍵,用於解決smembers
可能產生阻塞問題 -
hscan
命令用於迭代哈希鍵中的鍵值對,用於解決hgetall
可能產生阻塞問題。 -
zscan
:命令用於迭代有序集合中的元素(包括元素成員和元素分值),用於產生zrange
可能產生阻塞問題。
總結
Redis 使用單線程執行操做命令,全部客戶端發送過來命令,Redis 都會現放入隊列,而後從隊列中順序取出執行相應的命令。
若是任一任務執行過慢,就會影響隊列中其餘任務的,這樣在外部客戶端看來,遲遲拿不到 Redis 的響應,看起來就很阻塞了同樣。
因此不要在生產執行 keys
、smembers
、hgetall
、zrange
這類可能形成阻塞的指令,若是真須要執行,可使用相應的scan
命令漸進式遍歷,能夠有效防止阻塞問題。
< END >
若是你們喜歡咱們的文章,歡迎你們轉發,點擊在看讓更多的人看到。也歡迎你們熱愛技術和學習的朋友加入的咱們的知識星球當中,咱們共同成長,進步。
RabbitMQ 集羣高可用原理及實戰部署介紹
本文分享自微信公衆號 - Java極客技術(Javageektech)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。