一個 randomkey 命令導(dǎo)致的 Redis 事故。。

Java技術(shù)棧
www.javastack.cn
關(guān)注閱讀更多優(yōu)質(zhì)文章
最近在公司對redis做一些二次開發(fā)時,發(fā)現(xiàn)一個randomkey命令可能導(dǎo)致整個redis實例長時間阻塞的問題,redis版本為3.2.9,以此記錄。
問題
由于我們公司使用的是redis集群版Codis,Codis內(nèi)置的redis版本比較低,為3.2.9版本。
我們近期在做Codis雙機(jī)房時,需要對redis增加一些功能以此支持雙機(jī)房,在開發(fā)和測試中發(fā)現(xiàn),執(zhí)行randomkey命令有可能導(dǎo)致整個redis長時間阻塞的問題。
randomkey主要功能是在redis中隨機(jī)返回一個key出來,它隨機(jī)選取key的代碼如下。
robj?*dbRandomKey(redisDb?*db)?{
????dictEntry?*de;
????//?死循環(huán)從哈希表中找到一個不過期的key
????while(1)?{
????????sds?key;
????????robj?*keyobj;
????????//?從實例的哈希表里隨機(jī)一個元素
????????de?=?dictGetRandomKey(db->dict);
????????if?(de?==?NULL)?return?NULL;
????????//?獲取這個元素的key
????????key?=?dictGetKey(de);
????????keyobj?=?createStringObject(key,sdslen(key));
????????//?如果key已經(jīng)過期?則把這個key從實例中刪除
????????//?注意:expireIfNeeded對于過期的key只針對master有效
????????//?如果是slave則永遠(yuǎn)不會刪除key
????????if?(dictFind(db->expires,key))?{
????????????if?(expireIfNeeded(db,keyobj))?{
????????????????decrRefCount(keyobj);
????????????????continue;
????????????}
????????}
????????return?keyobj;
????}
}?
從上面代碼可以看出來,如果當(dāng)前是個slave,并且整個實例中存在大量已經(jīng)過期的key(key已過期,但redis還未來得及刪除key),執(zhí)行randomkey命令時,由于找不到不過期的key,那么這個邏輯就會陷入死循環(huán),阻塞住整個實例,整個實例不可用。
如果當(dāng)前是master,執(zhí)行randomkey命令時,redis會一直隨機(jī)選擇key,直到找到一個不過期的key,同時會把已經(jīng)過期的key從整個實例中刪除。
也就是說,在這種場景下,雖然不會長時間阻塞整個實例,但也會比執(zhí)行一個普通的命令耗時要久。如果你在一個大量已過期的實例上執(zhí)行randomkey命令,那可能會導(dǎo)致業(yè)務(wù)訪問redis變慢。
解決
我們對比了官方最新版的redis,已經(jīng)針對此問題進(jìn)行了修復(fù)。
robj?*dbRandomKey(redisDb?*db)?{
????dictEntry?*de;
????//?當(dāng)前實例全部都是過期key?最大循環(huán)100次
????int?maxtries?=?100;
????int?allvolatile?=?dictSize(db->dict)?==?dictSize(db->expires)
????//?死循環(huán)從哈希表中找到一個不過期的key
????while(1)?{
????????sds?key;
????????robj?*keyobj;
????????//?從實例的哈希表里隨機(jī)一個元素
????????de?=?dictGetRandomKey(db->dict);
????????if?(de?==?NULL)?return?NULL;
????????//?獲取這個元素的key
????????key?=?dictGetKey(de);
????????keyobj?=?createStringObject(key,sdslen(key));
????????//?如果key已經(jīng)過期?則把這個key從實例中刪除
????????//?注意:expireIfNeeded對于過期的key只針對master有效
????????//?如果是slave則永遠(yuǎn)不會刪除key
????????if?(dictFind(db->expires,key))?{
????????????//?如果整個實例都是過期key?在slave上執(zhí)行此命令最多循環(huán)100次?避免長時間阻塞
????????????if?(allvolatile?&&?server.masterhost?&&?--maxtries?==?0)?{
????????????????return?keyobj;
????????????}
????????????if?(expireIfNeeded(db,keyobj))?{
????????????????decrRefCount(keyobj);
????????????????continue;
????????????}
????????}
????????return?keyobj;
????}
}?
解決方案就是增加一個最大重試次數(shù),如果整個實例都是過期key,那么最多尋找maxtries次就返回,避免阻塞整個實例。
注意點
但要注意的是,如果達(dá)到了maxtries,那么返回的key是已經(jīng)過期的key,你雖然在randomkey中看到了這個key,但對這個key執(zhí)行其他命令時,還是拿不到這個key的。
這個方案只針對slave上執(zhí)行這個命令進(jìn)行了修復(fù),也就是不會再讓redis陷入死循環(huán)。
但在master上執(zhí)行這個命令還是會發(fā)生上述的變慢問題,如果你在使用redis時,經(jīng)常使用這個命令,同時實例中存在大量已經(jīng)過期的key,那么redis變慢很有可能是這個問題導(dǎo)致的。
最后,歡迎大家關(guān)注公眾號Java技術(shù)棧獲取更多Redis系列教程。
作者:Kaito
鏈接:kaito-kidd.com/2020/06/25/redis-randomkey-issue/





關(guān)注Java技術(shù)??锤喔韶?/strong>


