Redis源碼解析——雙向鏈表

來源:互聯網
上載者:User

標籤:null   http   釋放   name   sdn   定義   教科書   sea   決定   

        相對於之前介紹的字典和SDS字串庫,Redis的雙向鏈表庫則是非常標準的、教科書般簡單的庫。但是作為Redis源碼的一部分,我決定還是要講一講的。(轉載請指明出於breaksoftware的csdn部落格)

基本結構

        首先我們看鏈表元素的結構。因為是雙向鏈表,所以其基本元素應該有一個指向前一個節點的指標和一個指向後一個節點的指標,還有一個記錄節點值的空間

typedef struct listNode {    struct listNode *prev;    struct listNode *next;    void *value;} listNode;
        因為雙向鏈表不僅可以從頭開始遍曆,還可以從尾開始遍曆,所以鏈表結構應該至少有頭和尾兩個節點的指標。

        但是作為一個可以承載各種類型資料的鏈表,還需要鏈表使用者提供一些處理節點中資料的能力。因為這些資料可能是使用者自訂的,所以像複製、刪除、對比等操作都需要使用者來告訴架構。在《Redis源碼解析——字典結構》一文中,我們看到使用者建立字典時需要傳入的dictType結構體,就是一個承載資料的上述處理方法的載體。但是Redis在設計雙向鏈表時則沒有使用一個結構來承載這些方法,而是在鏈表結構中定義了

typedef struct list {    listNode *head;    listNode *tail;    void *(*dup)(void *ptr);    void (*free)(void *ptr);    int (*match)(void *ptr, void *key);    unsigned long len;} list;
        至於鏈表結構中為什麼要存鏈表長度欄位len,我覺得從必要性上來說是沒有必要的。有len欄位的一個優點是不用每次計算鏈表長度時都要做一次遍曆操作,缺點便是匯出需要維護這個變數。

建立和釋放鏈表

        鏈表建立的過程比較簡單。只是申請了一個list結構體空間,然後各個欄位設定為NULL

list *listCreate(void){    struct list *list;    if ((list = zmalloc(sizeof(*list))) == NULL)        return NULL;    list->head = list->tail = NULL;    list->len = 0;    list->dup = NULL;    list->free = NULL;    list->match = NULL;    return list;}
        但是比較有意思的是,建立鏈表時沒有設定鏈表類型——沒有設定複製、釋放、對比等方法的指標。作者單獨提供了一些宏來設定,個人覺得這種割裂開的設計不是很好

#define listSetDupMethod(l,m) ((l)->dup = (m))#define listSetFreeMethod(l,m) ((l)->free = (m))#define listSetMatchMethod(l,m) ((l)->match = (m))
        釋放鏈表的操作就是從頭部向尾部一個個釋放節點,迭代的方法是通過判斷不斷減小的鏈表長度欄位len是否為0來進行。之前說過,len其實沒有必要性,只要判斷節點的next指標為空白就知道到結尾了。

void listRelease(list *list){    unsigned long len;    listNode *current, *next;    current = list->head;    len = list->len;    while(len--) {        next = current->next;        if (list->free) list->free(current->value);        zfree(current);        current = next;    }    zfree(list);}
新增節點        新增節點分為三種:頭部新增、尾部新增和中間新增。頭部和尾部新增都很簡單,只是需要考慮一下新增之前鏈表是不是空的。如果是空的,要設定新增節點的指向前後指標為NULL,還要讓鏈表的頭尾指標都指向新增的節點

list *listAddNodeHead(list *list, void *value){    listNode *node;    if ((node = zmalloc(sizeof(*node))) == NULL)        return NULL;    node->value = value;    if (list->len == 0) {        list->head = list->tail = node;        node->prev = node->next = NULL;    } else {        node->prev = NULL;        node->next = list->head;        list->head->prev = node;        list->head = node;    }    list->len++;    return list;}list *listAddNodeTail(list *list, void *value){    listNode *node;    if ((node = zmalloc(sizeof(*node))) == NULL)        return NULL;    node->value = value;    if (list->len == 0) {        list->head = list->tail = node;        node->prev = node->next = NULL;    } else {        node->prev = list->tail;        node->next = NULL;        list->tail->next = node;        list->tail = node;    }    list->len++;    return list;}
        上述代碼還說明一個問題,建立節點的資料指標指向傳入的value內容,而沒有使用複製操作將value所指向的資料複製下來。於是插入鏈表中的資料,要保證在鏈表生存周期之內都要有效。

        在鏈表中間插入節點時,可以指定插入到哪個節點前還是後。這個情境下則需要考慮作為座標的節點是否為鏈表的頭結點或者尾節點;如果是,可能還要視新插入的節點的位置更新鏈表的頭尾節點指向。

list *listInsertNode(list *list, listNode *old_node, void *value, int after) {    listNode *node;    if ((node = zmalloc(sizeof(*node))) == NULL)        return NULL;    node->value = value;    if (after) {        node->prev = old_node;        node->next = old_node->next;        if (list->tail == old_node) {            list->tail = node;        }    } else {        node->next = old_node;        node->prev = old_node->prev;        if (list->head == old_node) {            list->head = node;        }    }    if (node->prev != NULL) {        node->prev->next = node;    }    if (node->next != NULL) {        node->next->prev = node;    }    list->len++;    return list;}
刪除節點

        刪除節點時要考慮節點是否為鏈表的頭結點或者尾節點。如果是則要更新鏈表的資訊,否則只要更新待刪除的節點前後節點指向關係。

void listDelNode(list *list, listNode *node){    if (node->prev)        node->prev->next = node->next;    else        list->head = node->next;    if (node->next)        node->next->prev = node->prev;    else        list->tail = node->prev;    if (list->free) list->free(node->value);    zfree(node);    list->len--;}
建立和釋放迭代器

        迭代器是一種輔助遍曆鏈表的結構,它分為向前迭代器和向後迭代器。我們在迭代器結構中可以發現方向類型變數

typedef struct listIter {    listNode *next;    int direction;} listIter;
        建立一個迭代器,需要指定方向,從而可以讓迭代器的next指標指向鏈表的頭結點或者尾節點

listIter *listGetIterator(list *list, int direction){    listIter *iter;    if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;    if (direction == AL_START_HEAD)        iter->next = list->head;    else        iter->next = list->tail;    iter->direction = direction;    return iter;}
        還可以通過下面兩個方法,讓迭代器類型發生轉變,比如可以讓一個向前的迭代器變成一個向後的迭代器。還可以讓這個迭代器指向另外一個鏈表,而非建立它時指向的鏈表。

void listRewind(list *list, listIter *li) {    li->next = list->head;    li->direction = AL_START_HEAD;}void listRewindTail(list *list, listIter *li) {    li->next = list->tail;    li->direction = AL_START_TAIL;}
        因為通過listGetIterator建立的迭代器是在堆上動態分配的,所以不使用時要釋放

void listReleaseIterator(listIter *iter) {    zfree(iter);}
迭代器遍曆        迭代器的遍曆其實就是簡單的通過節點向前向後指標訪問到下一個節點的過程
listNode *listNext(listIter *iter){    listNode *current = iter->next;    if (current != NULL) {        if (iter->direction == AL_START_HEAD)            iter->next = current->next;        else            iter->next = current->prev;    }    return current;}
鏈表複製        鏈表的複製過程就是通過一個從頭向尾訪問的迭代器,將元鏈表中的資料複製到建立的鏈表中。但是這兒有個地方需要注意下,就是複製操作可能分為深拷貝和淺拷貝。如果我們通過listSetDupMethod設定了資料的複製方法,則使用該方法進行資料的複製,然後將複製出來的新資料放到新的鏈表中。如果沒有設定,則只是把老鏈表中元素的value欄位賦值過去。
list *listDup(list *orig){    list *copy;    listIter iter;    listNode *node;    if ((copy = listCreate()) == NULL)        return NULL;    copy->dup = orig->dup;    copy->free = orig->free;    copy->match = orig->match;    listRewind(orig, &iter);    while((node = listNext(&iter)) != NULL) {        void *value;        if (copy->dup) {            value = copy->dup(node->value);            if (value == NULL) {                listRelease(copy);                return NULL;            }        } else            value = node->value;        if (listAddNodeTail(copy, value) == NULL) {            listRelease(copy);            return NULL;        }    }    return copy;}
尋找元素        尋找元素同樣是通過迭代器遍曆整個鏈表,然後視使用者是否通過listSetMatchMethod設定對比方法來決定是使用使用者定義的方法去對比,還是直接使用value去對比。如果是使用value直接去對比,則是強對比,即要求對比的資料和鏈表的資料在記憶體中位置是一樣的。
listNode *listSearchKey(list *list, void *key){    listIter iter;    listNode *node;    listRewind(list, &iter);    while((node = listNext(&iter)) != NULL) {        if (list->match) {            if (list->match(node->value, key)) {                return node;            }        } else {            if (key == node->value) {                return node;            }        }    }    return NULL;}
通過下標訪問鏈表        下標可以是負數,代表返回從後數第幾個元素。
listNode *listIndex(list *list, long index) {    listNode *n;    if (index < 0) {        index = (-index)-1;        n = list->tail;        while(index-- && n) n = n->prev;    } else {        n = list->head;        while(index-- && n) n = n->next;    }    return n;}
結尾節點前移為頭結點        這個方法在Redis代碼中使用比較多。它將鏈表最後一個節點移動到鏈表頭部。我想設計這麼一個方法是為了讓鏈表內容可以在無狀態記錄的情況下被均勻的輪詢到。
void listRotate(list *list) {    listNode *tail = list->tail;    if (listLength(list) <= 1) return;    /* Detach current tail */    list->tail = tail->prev;    list->tail->next = NULL;    /* Move it as head */    list->head->prev = tail;    tail->prev = NULL;    tail->next = list->head;    list->head = tail;}

Redis源碼解析——雙向鏈表

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.