memcached中的經典數據結構hash鏈表和LRU刪除策略的應用

hash鏈表是memcached核心的數據結構了,hash表用於快速讀取、寫入緩存數據(item結構),鏈表用於實現lru策略,lru用於內存不足時的兜底策略,有效防止了內存不足時形成宕機git

緩存操做

memcached中使用struct _stritem結構體表明緩存,這個結構體被typedef定義成了item類型,對這個item結構體的操做都在item.c中,函數列表以下github

函數 做用
item_init 初始化lru鏈表頭尾指針、item數量數組
item_make_header 計算一個item佔用的內存字節數
item_alloc 從slab中分配出item的內存
item_free 把item內存還給slab
item_size_ok 檢查item內存大小是否超過了slab容許的範圍
item_link_q 把item加入到lru鏈表頭
item_unlink_q 從鏈表中刪除item
item_link 把item插入hash表,同時加入lru鏈表中(item_link_q)
item_unlink 把item從hash表中刪除,同時從lru鏈表中刪除(item_unlink_q)
item_remove 把item內存還給slab,帶異常檢查
item_update 修改item,同時按照lru規則重建這個item在lru鏈表中的位置
item_replace 替換item,和item_update的區別時,這個不會修改item的最後訪問時間
item_cachedump 獲取指定slab的全部item的key和value的大小
item_stats 獲取全部lru鏈表的大小
item_stats_sizes 統計item的大小

能夠看到這個item的操做就用到了hash表和鏈表,item是在hash表和鏈表上進行的,即對緩存item的操做,會同時影響到hash表和鏈表兩種數據結構的構建c#

hash表構建

hash表的構建、crud操做都在assoc.c中數組

函數 做用
hash 將字符串char*散列成8字節32位的數字
assoc_init 爲hash表分配內存空間,默認大小是hashsize(HASHPOWER) * sizeof(void*)
assoc_find 經過hash表找到對應hash的value
assoc_insert 將新key插入hash表
assoc_delete 從hash表中刪除指定key的hash

hash衝突的解決

不一樣的key,經過hash函數可能hash成了同一個32位的數字,這不會影響hash表插入(純鏈表操做),可是查找的時候,就不能只靠hash找item了,memcached採用的是開放尋址,即對比找到的item的key是否和查找的key相同,如不一樣,就是遇到了hash衝突,繼續和鏈表的下一個item進行對比緩存

item *it = hashtable[hv];

while (it) {
    if (strcmp(key, ITEM_key(it)) == 0)
        return it;
    it = it->h_next;
}
return 0;
複製代碼

鏈表構建

lru鏈表定義在了items.c中,在item_init()函數中初始化成空指針bash

items.c數據結構

#define LARGEST_ID 255
static item *heads[LARGEST_ID];
static item *tails[LARGEST_ID];
複製代碼

item_init()memcached

for(i=0; i<LARGEST_ID; i++) {
    heads[i]=0;
    tails[i]=0;
}
複製代碼

LARGEST_ID常量是最大的內存slab數量,每一個slab對應了一個頭尾鏈表。函數

鏈表頭維護時機

add/set時添加

add/set/replace時會把狀態機置成conn_nread狀態: conn_set_state(c, conn_nread)ui

經過nread -> complete_nread(c) -> item_link(it) -> item_link_q(it) 再item_link_q中把item放到lru鏈表頭部(鏈表頭指向新增的元素),核心代碼以下

head = &heads[it->slabs_clsid];
tail = &tails[it->slabs_clsid];
it->prev = 0;
it->next = *head;
*head = it;
if (*tail == 0) *tail = it;
複製代碼

若是這時若是尾指針爲空,表示lru鏈表都是剛構建,這時尾指針和頭指針初始化都指向第一個it元素

replace時刪除

item被replace、delete時,都須要把item從lru鏈表中刪除,replace會調用item_link_q從新添加到lru鏈表中

item_unlink_q()核心代碼以下

item **head, **tail;

head = &heads[it->slabs_clsid];
tail = &tails[it->slabs_clsid];

if (*head == it) {
    *head = it->next;
}
if (*tail == it) {
    *tail = it->prev;
}

if (it->next) it->next->prev = it->prev;
if (it->prev) it->prev->next = it->next;
複製代碼

能夠看到若是被刪除的item正好是lru鏈表的頭/尾指針時,須要多維護一下頭尾指針,其餘狀況直接把item從lru鏈表中解除關係便可

鏈表尾維護時機

鏈表尾指針只會在lru鏈表都是剛構建的時候,指向第一個元素,以及刪除lru鏈表元素時,恰好刪到了尾指針,其它時候都不會再動了,這就保證了tail指針永遠指向lru鏈表的最後一個元素

lru生效時機

lru生效發生在memcached沒法從slab分配新內存的時候,會嘗試使用lru鏈表尾指針往前進行查找出能夠刪除的item,進行刪除釋放內存空間給新增的元素使用

核心代碼在items.c中,調用slabs_alloc(ntotal)分配內存失敗時

it = slabs_alloc(ntotal);
if (it == 0) {
    int tries = 50;
    item *search;
    if (tails[id]==0) return 0;

    for (search = tails[id]; tries>0 && search; tries--, search=search->prev) {
        if (search->refcount==0) {
            item_unlink(search);
            break;
        }
    }
    it = slabs_alloc(ntotal);
    if (it==0) return 0;
}
複製代碼

memcached一共會嘗試最後50個元素進行lru淘汰,若是仍然沒有元素可刪除,這個時候就直接返回錯誤了,最新版lru對這個簡單的策略改進了不少,參考:do_item_alloc_pull()

一些注意的點

本文基於memcached 1.2.0

telnet調試memcached時value跟在最後,命令以回車(\n)結束,核心代碼以下

el = memchr(c->rcurr, '\n', c->rbytes);
if (!el)
    return 0;
複製代碼

參考資料

  1. github.com/memcached/m…
  2. www.journaldev.com/16/memcache…
  3. blog.csdn.net/sky2098/art…
相關文章
相關標籤/搜索