文章來源于公眾號(hào):Java極客技術(shù) 作者:鴨血粉絲
哎,最近阿粉又雙叒叕犯事了。
事情是這樣的,前一段時(shí)間阿粉公司生產(chǎn)交易偶發(fā)報(bào)錯(cuò),一番排查下來最終原因是因?yàn)?Redis 命令執(zhí)行超時(shí)。
可是令人不解的是,生產(chǎn)交易僅僅使用 Redis set 這個(gè)簡(jiǎn)單命令,這個(gè)命令講道理是不可能會(huì)執(zhí)行這么慢。
那到底是什么導(dǎo)致這個(gè)問題那?
為了找出這個(gè)問題,我們查看分析了一下 Redis 最近的慢日志,最終發(fā)現(xiàn)耗時(shí)比較多命令為 keys XX*
看到這個(gè)命令操作的鍵的前綴,阿粉才發(fā)現(xiàn)這是自己負(fù)責(zé)的應(yīng)用??墒前⒎叟挪橐幌?,雖然自己的代碼并沒有主動(dòng)去使用 keys
命令,但是底層使用框架卻在間接使用,于是就有了今天這個(gè)問題。
問題原因
阿粉負(fù)責(zé)的應(yīng)用是一個(gè)管理后臺(tái)應(yīng)用,權(quán)限管理使用 Shiro 框架,由于存在多個(gè)節(jié)點(diǎn),需要使用分布式 Session,于是這里使用 Redis 存儲(chǔ) Session 信息。
由于 Shiro 并沒有直接提供 Redis 存儲(chǔ) Session 組件,阿粉不得不使用 Github 一個(gè)開源組件 shiro-redis。
由于 Shiro 框架需要定期驗(yàn)證 Session 是否有效,于是 Shiro 底層將會(huì)調(diào)用 SessionDAO#getActiveSessions
獲取所有的 Session 信息。
而 shiro-redis
正好繼承 SessionDAO
這個(gè)接口,底層使用用keys
命令查找 Redis 所有存儲(chǔ)的 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;
}
找到問題原因,解決辦法就比較簡(jiǎn)單了,github 上查找到解決方案,升級(jí)一下 shiro-redis
到最新版本。
在這個(gè)版本,shiro-redis
采用 scan
命令代替 keys
,從而修復(fù)這個(gè)問題。
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;
}
雖然問題成功解決了,但是阿粉心里還是有點(diǎn)不解。
為什么keys
指令會(huì)導(dǎo)致其他命令執(zhí)行變慢?
為什么Keys
指令查詢會(huì)這么慢?
為什么Scan
指令就沒有問題?
Redis 執(zhí)行命令的原理
首先我們來看第一個(gè)問題,為什么keys
指令會(huì)導(dǎo)致其他命令執(zhí)行變慢?
回答這個(gè)問題,我們首先看下 Redis 客戶端執(zhí)行一條命令的情況:
站在客戶端的視角,執(zhí)行一條命令分為三步:
- 發(fā)送命令
- 執(zhí)行命令
- 返回結(jié)果
但是這僅僅客戶端自己以為的過程,但是實(shí)際上同一時(shí)刻,可能存在很多客戶端發(fā)送命令給 Redis ,而 Redis 我們都知道它采用的是單線程模型。
為了處理同一時(shí)刻所有的客戶端的請(qǐng)求命令,Redis 內(nèi)部采用了隊(duì)列的方式,排隊(duì)執(zhí)行。
于是客戶端執(zhí)行一條命令實(shí)際需要四步:
- 發(fā)送命令
- 命令排隊(duì)
- 執(zhí)行命令
- 返回結(jié)果
由于 Redis 單線程執(zhí)行命令,只能順序從隊(duì)列取出任務(wù)開始執(zhí)行。
只要 3 這個(gè)過程執(zhí)行命令速度過慢,隊(duì)列其他任務(wù)不得不進(jìn)行等待,這對(duì)外部客戶端看來,Redis 好像就被阻塞一樣,一直得不到響應(yīng)。
所以使用 Redis 過程切勿執(zhí)行需要長(zhǎng)時(shí)間運(yùn)行的指令,這樣可能導(dǎo)致 Redis 阻塞,影響執(zhí)行其他指令。
KEYS 原理
接下來開始回答第二個(gè)問題,為什么Keys
指令查詢會(huì)這么慢?
回答這個(gè)問題之前,請(qǐng)大家回想一下 Redis 底層存儲(chǔ)結(jié)構(gòu)。
不太清楚朋友的也沒關(guān)系,大家可以回看一下之前的文章「阿里面試官:HashMap 熟悉吧?好的,那就來聊聊 Redis 字典吧!」。
這里阿粉復(fù)制之前文章內(nèi)容,Redis 底層使用字典這種結(jié)構(gòu),這個(gè)結(jié)構(gòu)與 Java HashMap 底層比較類似。
keys
命令需要返回所有的符合給定模式 pattern
的 Redis 中鍵,為了實(shí)現(xiàn)這個(gè)目的,Redis 不得不遍歷字典中 ht[0]
哈希表底層數(shù)組,這個(gè)時(shí)間復(fù)雜度為 「O(N)」(N 為 Redis 中 key 所有的數(shù)量)。
如果 Redis 中 key 的數(shù)量很少,那么這個(gè)執(zhí)行速度還是也會(huì)很快。等到 Redis key 的數(shù)量慢慢更加,上升到百萬、千萬、甚至上億級(jí)別,那這個(gè)執(zhí)行速度就會(huì)很慢很慢。
下面是阿粉本地做的一次實(shí)驗(yàn),使用 lua 腳本往 Redis 中增加 10W 個(gè) key,然后使用 keys
查詢所有鍵,這個(gè)查詢大概會(huì)阻塞十幾秒的時(shí)間。
eval "for i=1,100000 do redis.call('set',i,i+1) end" 0
這里阿粉使用 Docker 部署 Redis,性能可能會(huì)稍差。
SCAN 原理
最后我們來看下第三個(gè)問題,為什么scan
指令就沒有問題?
這是因?yàn)?scan
命令采用一種黑科技-「基于游標(biāo)的迭代器」。
每次調(diào)用 scan
命令,Redis 都會(huì)向用戶返回一個(gè)新的游標(biāo)以及一定數(shù)量的 key。下次再想繼續(xù)獲取剩余的 key,需要將這個(gè)游標(biāo)傳入 scan 命令, 以此來延續(xù)之前的迭代過程。
簡(jiǎn)單來講,scan
命令使用分頁(yè)查詢 redis 。
下面是一個(gè) scan 命令的迭代過程示例:
scan
命令使用游標(biāo)這種方式,巧妙將一次全量查詢拆分成多次,降低查詢復(fù)雜度。
雖然 scan
命令時(shí)間復(fù)雜度與 keys
一樣,都是 「O(N)」,但是由于 scan
命令只需要返回少量的 key,所以執(zhí)行速度會(huì)很快。
最后,雖然scan
命令解決 keys
不足,但是同時(shí)也引入其他一些缺陷:
- 同一個(gè)元素可能會(huì)被返回多次,這就需要我們應(yīng)用程序增加處理重復(fù)元素功能。
- 如果一個(gè)元素在迭代過程增加到 redis,或者說在迭代過程被刪除,那個(gè)這個(gè)元素會(huì)被返回,也可能不會(huì)。
以上這些缺陷,在我們開發(fā)中需要考慮這種情況。
除了 scan
以外,redis 還有其他幾個(gè)用于增量迭代命令:
sscan
:用于迭代當(dāng)前數(shù)據(jù)庫(kù)中的數(shù)據(jù)庫(kù)鍵,用于解決smembers
可能產(chǎn)生阻塞問題hscan
命令用于迭代哈希鍵中的鍵值對(duì),用于解決hgetall
可能產(chǎn)生阻塞問題。zscan
:命令用于迭代有序集合中的元素(包括元素成員和元素分值),用于產(chǎn)生zrange
可能產(chǎn)生阻塞問題。
總結(jié)
Redis 使用單線程執(zhí)行操作命令,所有客戶端發(fā)送過來命令,Redis 都會(huì)現(xiàn)放入隊(duì)列,然后從隊(duì)列中順序取出執(zhí)行相應(yīng)的命令。
如果任一任務(wù)執(zhí)行過慢,就會(huì)影響隊(duì)列中其他任務(wù)的,這樣在外部客戶端看來,遲遲拿不到 Redis 的響應(yīng),看起來就很阻塞了一樣。
所以不要在生產(chǎn)執(zhí)行 keys
、smembers
、hgetall
、zrange
這類可能造成阻塞的指令,如果真需要執(zhí)行,可以使用相應(yīng)的scan
命令漸進(jìn)式遍歷,可以有效防止阻塞問題。
以上就是W3Cschool編程獅
關(guān)于想在生產(chǎn)搞事情?那試試這些 Redis 命令的相關(guān)介紹了,希望對(duì)大家有所幫助。