本文原文發(fā)表在我的個(gè)人博客扶檐。
redis是一款開源的內(nèi)存數(shù)據(jù)存儲(chǔ)系統(tǒng)迹缀,可以用作數(shù)據(jù)庫、緩存甚至是消息中間件(pub/sub)來使用种远。與memcache相比涩澡,redis支持更多的數(shù)據(jù)結(jié)構(gòu),比如string,hash,list,set,bit map,sorted set甚至是geo等等坠敷,基本覆蓋了日常開發(fā)中使用到的數(shù)據(jù)結(jié)構(gòu)妙同。而且redis十分高效,當(dāng)然是建立在合理使用的前提下膝迎。由于redis單線程的設(shè)計(jì)特性渐溶,任何一條阻塞的命令都會(huì)引起redis整個(gè)實(shí)例的阻塞,所以在使用過程中弄抬,需要盡量避免過多使用時(shí)間復(fù)雜度高的命令(特別是高并發(fā)的環(huán)境下)茎辐,一般我們可以通過查看redis command的時(shí)間復(fù)雜度,但實(shí)際使用情況中還是會(huì)遇到不少坑,本文主要記錄工作中遇到的redis問題拖陆,持續(xù)更新~~~
找出引起redis慢的原因
slowlog get
通常我們可以通過redis慢日志來找到引起redis慢的命令弛槐,用法為slowlog get 10
來查看最慢的10條命令,然后針對(duì)性的進(jìn)行優(yōu)化依啰。慢日志可以通過redis.conf或者運(yùn)行時(shí)通過:
CONFIG SET slowlog-log-slower-than 5000
CONFIG SET slowlog-max-len 25
來設(shè)置slowlog參數(shù)乎串,其中slowlog-log-slower-than
表示執(zhí)行時(shí)間超過該值(單位毫秒)的命令記為慢查詢,slowlog-max-len
可以設(shè)置記錄的最大條數(shù)速警√居可以通過slowlog reset
命令來重置慢日志記錄。
info commandstats
info commandstats
命令會(huì)告訴你整個(gè)redis執(zhí)行了哪些命令闷旧、分別執(zhí)行了多少次长豁、總計(jì)耗時(shí)、平均每次耗時(shí)等信息忙灼,同時(shí)可以通過config resetstat
命令來重置統(tǒng)計(jì)匠襟。
client list
redis-cli -h localhost -p 6379 client list | grep -v "omem=0"
這條命令在排查redis慢的時(shí)候絕對(duì)是神技。一般阻塞的命令都會(huì)導(dǎo)致omem不斷升高该园,這條命令能快速找到引起阻塞的命令酸舍,返回的數(shù)據(jù):
id=1212 addr=10.10.10.10:34234 fd=11 name= age=3242 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=232432 omem=23132432 events=rw cmds=keys *
引起阻塞的具體命令,甚至發(fā)起命令的機(jī)器ip都能查到里初。
scan instead of keys *
在key數(shù)量較少的情況下啃勉,我們可以偷懶直接使用keys *
來快速查看模式匹配的key列表,但是一旦key數(shù)量上升双妨,或者在高并發(fā)的環(huán)境下璧亮,keys *
會(huì)帶來整個(gè)系統(tǒng)的阻塞。因?yàn)?code>keys命令時(shí)間復(fù)雜度是:
Time complexity:O(N) with N being the number of keys in the database, under the assumption that the key names in the database and the given pattern have limited length.
直接跟database中key數(shù)量相關(guān)的斥难。替代方案是使用scan
命令枝嘶,首先看看scan
的時(shí)間復(fù)雜度:
Time complexity:O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.
雖然scan不能一次性返回所有匹配的key,但是scan提供cursor機(jī)制來遍歷整個(gè)database哑诊,最重要的是每次scan操作的時(shí)間復(fù)雜度是O(1)的群扶,因此只需要多次scan即可得到所有匹配的key。類似的命令還有sscan
,hscan
,zscan
等分別用于增量迭代set,hash,sorted set等集合元素镀裤。
var scan = function(offset){
(function(offset){
redis.scan([offset, 'match', 'key_pattern_*', 'count', 1000], function(err, ret){
if(!!err){
console.error(err);
process.exit(-1);
}
if(Number(ret[0] === 0)){
console.log('scan finished');
process.exit(0);
}
// matched keys in ret[1]
// deal with keys
// scan again.
scan(Number(ret[0]));
});
})(offset);
};
scan(0);
使用redis slave
同使用其他數(shù)據(jù)庫一樣竞阐,讀寫分離總是能極大的提升系統(tǒng)在高并發(fā)情況下的性能,redis也不例外暑劝。
一主多從
通常一主多從可以用來實(shí)現(xiàn)讀寫分離骆莹,而且能間接的保證數(shù)據(jù)安全性(master掛了slave還有數(shù)據(jù)),所以通常比較好的部署結(jié)構(gòu)是M-S-S担猛,即在slave下部署slave而不是全部部署為master的slave幕垦,這樣master掛了可以將一級(jí)slave快速切換為master使用丢氢。
多寫
多寫一般用于寫操作很頻繁的情況,這時(shí)一般需要業(yè)務(wù)上進(jìn)行額外的處理先改,或者更好的辦法是增加redis proxy類的中間件來對(duì)業(yè)務(wù)隔離多寫的復(fù)雜性疚察。
DEL操作隱藏的問題
之前一直以為del
操作不會(huì)有性能問題,直到我的膝蓋中了一箭...
因?yàn)橹挥浀?code>del對(duì)于string和hash的時(shí)間復(fù)雜度是O(1)的仇奶,但是對(duì)于list,set,sorted set等居然是O(N)的貌嫡,所以當(dāng)你準(zhǔn)備使用del來刪除一個(gè)有百萬級(jí)數(shù)據(jù)的集合,那你就準(zhǔn)備阻塞吧...
Time complexity:O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).
我們的方案是:不直接刪除這種大的集合该溯,而是將他們重命名(確認(rèn)是O(1)的岛抄,不用擔(dān)心:D),然后后臺(tái)跑一個(gè)刪除進(jìn)程慢慢刪狈茉。夫椭。。
首先论皆,將程序中del大集合修改為rename:
//cmds.push(['del', 'bigset']);
cmds.push(['rename', 'bigset', 'gc:bigset']);
接下來部署刪除函數(shù):
var delHugeSet = function(key, cb){
redis.scard(key, function(err, size){
if(size>500){
redis.srandmember(key, 500, function(err, ids){
//分批慢慢刪
redis.srem(key, ids, function(err, ret){
delHugeSet(key, cb);
});
});
}else{
//數(shù)據(jù)量不大,直接del
redis.del(key, function(err, ret){
cb();
});
}
});
};