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

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

1. 問題簡述

前幾天接收到報(bào)警,同時(shí)Redis團(tuán)隊(duì)監(jiān)控到redis集群發(fā)生了主從切換;

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

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

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

2. 解決方案

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

我們可以一點(diǎn)一點(diǎn)刪除集合中的元素;

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

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

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

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

3. 更好的解決方案

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

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

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

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

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

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

redis 4.0刪除元素有兩個(gè)命令,del和unlink;del和之前版本一樣,直接刪除對(duì)象,可能會(huì)阻塞主進(jìn)程,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ù)第二個(gè)參數(shù)是lazy標(biāo)志;0同步刪除,1惰性/異步刪除,先解除對(duì)象數(shù)據(jù)庫字典關(guān)聯(lián)關(guān)系,再調(diào)用后臺(tái)線程釋放對(duì)象空間;

//lazy表示是否懶刪除

void delGenericCommand(client *c, int lazy) {

? ? int numdel = 0, j;

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

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

? ? ? ? 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);

}

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

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

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

? ? mstime_t now;

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

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

? ? if (server.loading) return 0;

? ? //slave機(jī)器,不處理

? ? 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);

}

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

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

#define LAZYFREE_THRESHOLD 64

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

? ? //刪除過期字典

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

? ? //從字典刪除鍵值對(duì),并返回

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

? ? if (de) {

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

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

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

? ? ? ? //開啟bio后臺(tái)線程刪除

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

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

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

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

? ? ? ? }

? ? }

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

? ? 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 過期刪除

對(duì)于過期鍵有三種檢測(cè)策略:

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

2.周期性檢測(cè):周期性檢測(cè)若干key過期時(shí)間,過期則刪除;

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

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

過期鍵刪除函數(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存儲(chǔ)的是字符串對(duì)象;過期字典key存儲(chǔ)的是sds

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

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

? ? ? ? if (server.lazyfree_lazy_expire)? ? //過期刪除時(shí),是否執(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é)

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

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

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

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

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