【Redis5源碼學習】淺析redis命令之del篇

baiyanredis

命令語法

命令含義:刪除一個鍵所對應的值
命令格式:數據庫

DEL [key1 key2 …]

命令實戰:segmentfault

127.0.0.1:6379> del key1
(integer) 1

返回值:被刪除 key 的數量數組

源碼分析

首先咱們開啓一個redis客戶端,使用gdb -p redis-server的端口。因爲del命令對應的處理函數是delCommand(),因此在delCommand處打一個斷點,而後在redis客戶端中執行如下幾個命令:異步

127.0.0.1:6379> set key1 value1 EX 100
OK
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> del key1

首先設置一個鍵值對爲key1-value一、過時時間爲100秒的鍵值對,而後在100秒以內對其進行刪除,執行del key1,,刪除這個尚未過時的鍵。咱們看看redis服務端是如何執行的:函數

Breakpoint 1, delCommand (c=0x7fb46230dec0) at db.c:487
487        delGenericCommand(c,0);
(gdb) s
delGenericCommand (c=0x7fb46230dec0, lazy=0) at db.c:468
468    void delGenericCommand(client *c, int lazy) {
(gdb) n
471        for (j = 1; j < c->argc; j++) {
(gdb) p c->argc 
$1 = 2
(gdb) p c->argv[0]
$2 = (robj *) 0x7fb462229800
(gdb) p *$2
$3 = {type = 0, encoding = 8, lru = 8575647, refcount = 1, ptr = 0x7fb462229813}

咱們看到,delCommand()直接調用了delGenericCommand(c,0),貌似是一個封裝好的通用刪除函數,咱們s進去,看下內部的執行流程。源碼分析

void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;

    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]); //看這個鍵是否過時,過時須要額外作一些其餘操做
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) : 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++; //AOF持久化相關
            numdel++; //刪除鍵的數量++,返回刪除的數量就是這個值
        }
    }
    addReplyLongLong(c,numdel);
}

首先直接進行了一個for循環,循環次數爲c->argc,咱們打印出來是2。看這個變量名咱們能夠猜想,多是del和key1這兩個命令參數。咱們打印c->argv[0],發現它是一個redis-object,而後咱們看它的encoding所對應的類型爲8,即embstr類型,一種優化版的字符串存儲形式。爲了查看它的內容,將該指針強轉爲char *:優化

(gdb) p *((char *)($3.ptr)) 
$4 = 100 'd'
(gdb) p *((char *)($3.ptr+1))
$6 = 101 'e'
(gdb) p *((char *)($3.ptr+2))
$7 = 108 'l'
(gdb) p *((char *)($3.ptr+3))

而後打印下一個參數:atom

(gdb) p *c->argv[1]
$10 = {type = 0, encoding = 8, lru = 8575647, refcount = 1, ptr = 0x7fb4622297e3}
(gdb) p *(char *)($10.ptr)
$11 = 107 'k'
(gdb) p *((char *)($10.ptr+1))
$12 = 101 'e'
(gdb) p *((char *)($10.ptr+2))
$13 = 121 'y'
(gdb) p *((char *)($10.ptr+3))
$14 = 49 '1'

咱們看到,c->argv數組中存儲的就是del和key1的值。可是下面的for循環是從1開始,沒有包含命令的名稱,因此只對key1這個參數進行了一次循環。隨後,調用expireIfNeeded()函數:spa

472            expireIfNeeded(c->db,c->argv[j]);
(gdb) s
expireIfNeeded (db=0x7fb46221a800, key=0x7fb4622297d0) at db.c:1167
1167    int expireIfNeeded(redisDb *db, robj *key) {
(gdb) n
1168        if (!keyIsExpired(db,key)) return 0;
(gdb) 
1187    }
(gdb)

咱們發現,它判斷了鍵是否過時,若是沒有過時,那就直接返回0。那麼看來,對於刪除命令,過時和非過時的刪除是有區別的。先跳過這裏,繼續往下走:

473            int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) : dbSyncDelete(c->db,c->argv[j]);

這裏有一個lazy的參數,lazy參數決定了後面是調用dbAsyncDelete()仍是dbSyncDelete()函數,咱們打印一下這個值:

(gdb) p lazy
$15 = 0

lazy的值爲0。看字面意思,好像表明不須要懶刪除的意思,非懶刪除對應的是dbSyncDelete()函數,即同步刪除。這裏咱們能夠知道,非懶刪除對應同步刪除。咱們跟進dbSyncDelete(c->db,c->argv[j]):

int dbSyncDelete(redisDb *db, robj *key) {
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr); //刪除key1對應的過時時間字典entry
    if (dictDelete(db->dict,key->ptr) == DICT_OK) { // 刪除字典中的key1-value1鍵值對
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

在redis中,過時時間和數據是分別存在redis數據庫下面的兩個字典中的。字典的結構咱們已討論過。在過時時間字典中,key就是咱們的key key1;而鍵空間字典會存儲真正的key-value對。因此要去字典中分別刪除過時時間以及數據的值。具體的刪除邏輯咱們先不深刻去了解,只知道過時時間和key-value對是存在字典dict中就好:

typedef struct redisDb {
    ...
    dict *dict;        // 鍵空間字典,保存數據庫中全部鍵值對
    dict *expires      // 過時時間字典,保存鍵的過時時間
    ...
} redisDb;

接續往下走,刪除成功後,deleted變量值爲1,會執行下面的if,並調用兩個函數,作一些事務、發佈/訂閱的操做,咱們不深刻講解這兩塊,而後會將刪除的數量++,而後將結果存到輸出緩衝區,命令執行結束。

擴展

懶刪除(lazy delete)

以前咱們的例子是調用了dbSyncDelete()方法,是同步來進行刪除操做的。可是若是lazy的值爲true,即若是開啓了懶刪除策略,就會調用dbAsyncDelete()方法:

#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {

    // 從過時鍵字典中刪除過時鍵的時間
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // 找到key在鍵空間字典中的位置指針
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        // 經過鍵指針獲取值
        robj *val = dictGetVal(de);
        // 計算並判斷是否須要懶刪除。若是隻刪除一個很小的key,不須要採用懶刪除策略,直接同步刪除便可。
        size_t free_effort = lazyfreeGetFreeEffort(val);
        // 當代價超過閾值64的時候,就會將懶刪除任務分發給後臺線程去作,不阻塞主進程
        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;
    }
}

咱們能夠看到,在懶刪除以前須要計算當前的key-value對是否適合懶刪除。當懶刪除超出閾值64的時候纔會開啓懶刪除策略。因此,咱們知道,它使用異步線程對刪除的鍵值對,進行延後內存回收。那麼爲何要這樣作呢?緣由是若是所要刪除的鍵值對所佔用的內存空間很是大,一次性作同步刪除的時間是很是久的,這樣會致使主進程一直處於阻塞狀態,沒法爲外部提供服務。因此,爲了解決這個問題,redis在4.0版本中提出了懶刪除的概念,經過異步線程的方式,解決了大key刪除時的阻塞問題。異步線程在Redis內部有一個特別的名稱,它就是BIO,全稱是Background IO,意思是在背後默默幹活的IO線程,它就是懶刪除的載體與核心。

參考資料

【Redis源碼分析】Redis 懶刪除(lazy free)簡史

相關文章
相關標籤/搜索