刪除大key導(dǎo)致redis主從切換

原文地址:https://blog.csdn.net/luoyu_/article/details/83090576

1. 問題簡述

前幾天接收到報警,同時Redis團隊監(jiān)控到redis集群發(fā)生了主從切換;

最終分析原因是,刪除大key,導(dǎo)致redis主服務(wù)器阻塞,sentinel哨兵認(rèn)為主服務(wù)器宕機,進行了故障轉(zhuǎn)移;如下圖所示:

在Redis集群中,應(yīng)用程序盡量避免使用大鍵;直接影響容易導(dǎo)致集群的容量和請求出現(xiàn)”傾斜問題“,同時在刪除大鍵或者打鍵過期時,容易出現(xiàn)故障切換和應(yīng)用程序雪崩的故障;

查詢線上有一個集合鍵,集合oea_set_star_ol_2017元素個數(shù)達到4300萬;當(dāng)刪除這個鍵,或者鍵過期時,會阻塞redis主進程,從而發(fā)生了主從切換;(集合中的每個元素對象都要釋放內(nèi)存空間,時間復(fù)雜度比較高)

2. 解決方案

眾所周知,Redis是單進程執(zhí)行命令請求的;集合已經(jīng)有4000多萬元素了,想要刪除這個集合,肯定不能直接刪除,否則必會阻塞主進程;

我們可以一點一點刪除集合中的元素;

Redis 2.8以上版本提供了這么一個命令:SCAN 命令,其相關(guān)的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令;

它們每次執(zhí)行都只會返回少量元素;(而不會出現(xiàn)像 KEYS命令、 SMEMBERS 命令帶來問題 —— 當(dāng) KEYS 命令被用于處理一個大的數(shù)據(jù)庫時, 又或者 SMEMBERS 命令被用于處理一個大的集合鍵時, 它們可能會阻塞服務(wù)器達數(shù)秒之久。)

我們可以這樣做:通過HSCAN,每次獲取500個字段,再用HDEL命令,每次刪除1個字段;

這樣雖然刪除過程時間復(fù)雜度也很高(提高客戶端復(fù)雜度,需要多次獲取key,批量執(zhí)行刪除命令),但是至少不會阻塞redis服務(wù)器。

3. 更好的解決方案

redis也發(fā)現(xiàn)了這個問題:直接使用del命令刪除大key會導(dǎo)致Redis主進程阻塞;分批次刪除,客戶端復(fù)雜度又比較高;

因此在Redis 4.0 的時候,提出了惰性刪除lazyfree:當(dāng)用戶刪除集key時,或者集合key過期需要刪除時,檢測如果集合元素大于64個,則使用惰性刪除,只解除集合對象與數(shù)據(jù)庫字典的關(guān)系,將集合對象放入待刪除隊列中,后臺現(xiàn)成依次獲取隊列中的對象,并真正的刪除;

redis 4.0 引入了lazyfree的機制,它可以將刪除鍵或數(shù)據(jù)庫的操作放在后臺線程里執(zhí)行, 從而盡可能地避免服務(wù)器阻塞。

lazyfree的原理不難想象,就是在刪除對象時只是進行邏輯刪除,然后把對象丟給后臺,讓后臺線程去執(zhí)行真正的destruct,避免由于對象體積過大而造成阻塞

下面我們深入redis源碼,分析redis惰性刪除策略;我們分析兩個方面:客戶端使用命令刪除大key,大key過期刪除;

3.1 客戶端使用命令刪除大key

redis 4.0刪除元素有兩個命令,del和unlink;del和之前版本一樣,直接刪除對象,可能會阻塞主進程,unlink就是惰性刪除;

下面看看del和unlink命令的代碼邏輯:

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0}

{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},

void delCommand(client *c) {

? ? delGenericCommand(c,0);

}

void unlinkCommand(client *c) {

? ? delGenericCommand(c,1);

}

delGenericCommand函數(shù)第二個參數(shù)是lazy標(biāo)志;0同步刪除,1惰性/異步刪除,先解除對象數(shù)據(jù)庫字典關(guān)聯(lián)關(guān)系,再調(diào)用后臺線程釋放對象空間;

//lazy表示是否懶刪除

void delGenericCommand(client *c, int lazy) {

? ? int numdel = 0, j;

? ? for (j = 1; j < c->argc; j++) {

? ? ? ? expireIfNeeded(c->db,c->argv[j]); //校驗對象是否過期(順便說一下,redis數(shù)據(jù)庫有兩個字典:對象字典 存儲鍵值對,過期時間字典 存儲鍵和過期時間)

? ? ? ? int deleted? = lazy ? dbAsyncDelete(c->db,c->argv[j]) : //根據(jù)lazy表示執(zhí)行同步/異步刪除操作

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dbSyncDelete(c->db,c->argv[j]);

? ? ? ? if (deleted) {

? ? ? ? ? ? signalModifiedKey(c->db,c->argv[j]);

? ? ? ? ? ? notifyKeyspaceEvent(NOTIFY_GENERIC,

? ? ? ? ? ? ? ? "del",c->argv[j],c->db->id);

? ? ? ? ? ? server.dirty++;

? ? ? ? ? ? numdel++;

? ? ? ? }

? ? }

? ? addReplyLongLong(c,numdel);

}

刪除命令之前如果檢測到這個key已過期,則執(zhí)行過期刪除操作;

int expireIfNeeded(redisDb *db, robj *key) {

? ? mstime_t when = getExpire(db,key);

? ? mstime_t now;

? ? if (when < 0) return 0; //key沒有配置過期時間

? ? //正在加載db,直接返回

? ? if (server.loading) return 0;

? ? //slave機器,不處理

? ? if (server.masterhost != NULL) return now > when;

? ? //沒有到期

? ? if (now <= when) return 0;

? ? //刪除

? ? server.stat_expiredkeys++;

? ? propagateExpire(db,key,server.lazyfree_lazy_expire); //傳播到期刪除命令給aof和slaves

? ? notifyKeyspaceEvent(NOTIFY_EXPIRED,

? ? ? ? "expired",key,db->id);

? ? return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : //根據(jù)過期刪除策略決定同步/異步刪除(用戶可配置)

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dbSyncDelete(db,key);

}

惰性刪除時,會執(zhí)行異步刪除函數(shù)

//異步刪除函數(shù):

#define LAZYFREE_THRESHOLD 64

int dbAsyncDelete(redisDb *db, robj *key) {

? ? //刪除過期字典

? ? if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

? ? //從字典刪除鍵值對,并返回

? ? dictEntry *de = dictUnlink(db->dict,key->ptr);

? ? if (de) {

? ? ? ? robj *val = dictGetVal(de);

? ? ? ? size_t free_effort = lazyfreeGetFreeEffort(val); //獲得當(dāng)前對象長度(列表元素數(shù)目,hash對象鍵值對數(shù)目。。。)

? ? ? ? //當(dāng)對象元素超過64個,且對象引用計數(shù)為1,才會懶刪除;

? ? ? ? //開啟bio后臺線程刪除

? ? ? ? if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {

? ? ? ? ? ? atomicIncr(lazyfree_objects,1);

? ? ? ? ? ? bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);? //子線程刪除

? ? ? ? ? ? dictSetVal(db->dict,de,NULL);

? ? ? ? }

? ? }

? ? //釋放鍵值對(假如懶釋放,這里只釋放鍵對象)

? ? if (de) {

? ? ? ? dictFreeUnlinkedEntry(db->dict,de);

? ? ? ? if (server.cluster_enabled) slotToKeyDel(key);

? ? ? ? return 1;

? ? } else {

? ? ? ? return 0;

? ? }

}

//同步刪除函數(shù),直接刪除

int dbSyncDelete(redisDb *db, robj *key) {

? ? /* Deleting an entry from the expires dict will not free the sds of

? ? * the key, because it is shared with the main dictionary. */

? ? if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

? ? if (dictDelete(db->dict,key->ptr) == DICT_OK) {

? ? ? ? if (server.cluster_enabled) slotToKeyDel(key);

? ? ? ? return 1;

? ? } else {

? ? ? ? return 0;

? ? }

}

3.2 過期刪除

對于過期鍵有三種檢測策略:

1.添加定時器:設(shè)置key過期時間時,添加定時器,定時執(zhí)行過期刪除(沒有這么做)

2.周期性檢測:周期性檢測若干key過期時間,過期則刪除;

3.訪問這個key時,如果已經(jīng)過期,則刪除

redis結(jié)合2和3兩種策略,實現(xiàn)過期鍵的檢測;

過期鍵刪除函數(shù)如下所示:

//過期鍵刪除函數(shù)

int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {

? ? long long t = dictGetSignedIntegerVal(de);

? ? if (now > t) {

? ? ? ? sds key = dictGetKey(de);

? ? ? ? robj *keyobj = createStringObject(key,sdslen(key)); //數(shù)據(jù)庫字典key存儲的是字符串對象;過期字典key存儲的是sds

? ? ? ? //代碼基本與刪除key代碼相同;

? ? ? ? propagateExpire(db,keyobj,server.lazyfree_lazy_expire);

? ? ? ? if (server.lazyfree_lazy_expire)? ? //過期刪除時,是否執(zhí)行異步刪除操作,由用戶配置,server.lazyfree_lazy_expire

? ? ? ? ? ? dbAsyncDelete(db,keyobj);

? ? ? ? else

? ? ? ? ? ? dbSyncDelete(db,keyobj);

? ? ? ? notifyKeyspaceEvent(NOTIFY_EXPIRED,

? ? ? ? ? ? "expired",keyobj,db->id);

? ? ? ? decrRefCount(keyobj);

? ? ? ? server.stat_expiredkeys++;

? ? ? ? return 1;

? ? } else {

? ? ? ? return 0;

? ? }

}

4.總結(jié)

對于大key刪除,上面提出了兩種方案

對于低版本redis 2.8以上 4.0以下:使用scan命令分批次獲得大key中的元素,分批次刪除,直到刪除大key中的所有元素;

客戶端刪除大key時,使用unlink命令,其會執(zhí)行惰性刪除策略,只是邏輯刪除大key,真正的刪除是在后臺線程進行的;而對于過期刪除,則需要用戶配置server.lazyfree_lazy_expir,這樣redis在刪除過期鍵時,才會執(zhí)行惰性刪除策略。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容