接上一篇,我們得知了redis中存在大KEY,那么這個大KEY如何刪除呢?本文將從源碼角度分析Redis4.0帶來的新特性。
在Redis中,對於大KEY的刪除一直是個比較頭疼的問題,為了不影響服務,我們通常需要自己實現工具來刪除大KEY,或者在業務低峰期進行刪除操作。
為了解決以上問題, Redis 4.0 新添加了 UNLINK 命令用於執行大KEY異步刪除。那么這個異步刪除的背后的邏輯是什么?
通過源碼我們可以的得知以下信息:
當我們調用異步刪除UNLINK命令時:
- 釋放掉Expire Dicti 對 K-V 的引用
- 釋放Main Dict 對 K-V 的引用,同時記錄下這個K-V 的 Entry地址
- 計算釋放掉這個V 所需要的代價,計算方法如下:
3.1 如果這個V 是一個 String 類型,則代價為 1
3.2 如果這個V 是一個復合類型,則代價為 該復合類型的長度,比如,list 則為 llen 的結果,hash 則為 hlen 的結果 … - 根據得到的代價值,和代價閾值比對,如果小於 64 則,可以直接釋放掉K-V 內存空間;如果大於 64 則,把該V 放入lazyfree 隊列中,同時啟動一個BIO后台JOB進行刪除
4.1 在后台線程對 V 進行刪除時,也是根據不同類型的 V 做不同的操作
4.2 如果是 LIST 類型,則根據LIST 長度,則直接釋放空間。
4.3 如果是 SET 類型,並且數據結構采用 HASH 表存儲,那么遍歷整個hash表,逐個釋放 k,v空間;如果數據結構采用 intset,則直接釋放空間即可
4.4 如果是 ZSET 類型,並且數據結構采用 SKIPLIST 存儲,由於 SKIPLIST 底層采用 HASH + skiplist 存儲,那么會先釋放掉 SKIPLIST 中 hash 存儲空間,再釋放掉 SKIPLIST 中 skiplist 部分; 如果數據結構采用 ZIPLIST 存儲,則直接釋放空間。
4.5 如果是 HASH 類型,並且數據結構采用 HASH表存儲,則遍歷整個hash表,逐個釋放 k,v空間;如果數據結構采用 ZIPLIST 存儲,則直接釋放空間。 - 設置 V 值等於NULL
- 釋放掉 K-V 空間
異步刪除代碼如下 :
int dbAsyncDelete(redisDb *db, robj *key) {
/* */
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
/* 在Main Dict 鏈表去掉引用,得到K-V entryDict */
dictEntry *de = dictUnlink(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
size_t free_effort = lazyfreeGetFreeEffort(val);
/* 計算DEL key 的代價,根據代價決定是否采用異步刪除方式 */
if (free_effort > LAZYFREE_THRESHOLD) {
atomicIncr(lazyfree_objects,1,lazyfree_objects_mutex);
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
}
/* 釋放K-V空間,或者采用了異步刪除方式,只需要釋放Key空間 */
if (de) {
dictFreeUnlinkedEntry(db->dict,de);
if (server.cluster_enabled) slotToKeyDel(key);
return 1;
} else {
return 0;
}
}
/* 釋放LIST 空間 */
void quicklistRelease(quicklist *quicklist) {
unsigned long len;
quicklistNode *current, *next;
current = quicklist->head;
len = quicklist->len;
while (len--) {
next = current->next;
zfree(current->zl);
quicklist->count -= current->count;
zfree(current);
quicklist->len--;
current = next;
}
zfree(quicklist);
}
/* 釋放HASH表空間 */
static int _dictClear(dict *ht) {
unsigned long i;
for (i = 0; i < ht->size && ht->used > 0; i++) {
dictEntry *he, *nextHe;
if ((he = ht->table[i]) == NULL) continue;
while(he) {
nextHe = he->next;
dictFreeEntryKey(ht, he);
dictFreeEntryVal(ht, he);
free(he);
ht->used--;
he = nextHe;
}
}
free(ht->table);
_dictReset(ht);
return DICT_OK; /* never fails */
}
由於異步刪除實際上是先在MAIN DICT 里邊把 這個K,V 的引用關系去掉了,所以當我們再次查詢這個Key 的時候是查不到的,然后在慢慢釋放Value 所占用的內存空間。
我們發現在異步進行刪除的時候,不管是刪除 HASH也好,還是QUICKLIST 也罷,這部分其實並沒有進行一個速度的控制,只是起了一個線程讓他去刪除,能跑多快就跑多快,這樣可能會導致我們在進行刪除的時候CPU飆高。
這個刪除大KEY是在Master 上進行的,如果這個節點有Slave呢?slave 會進行怎樣的操作?同樣根據代碼可以發現,我們在執行UNLINK操作時,實際上在 AOF 和 通知Slave的時候只是發送了一條DEL xxkey 命令,當slave 收到del命令時,會采取以上同樣的判斷對這個key進行刪除。
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[j],c->db->id);