LRU及其在InnoDB、Redis中的使用

一.頁面置換算法

       地址映射過程中,若在頁面中發現所要訪問的頁面不在內存中,則產生缺頁中斷。當發生缺頁中斷時,如果操作系統內存中沒有空閒頁面,則操作系統必須在內存選擇一個頁面將其移出內存,以便爲即將調入的頁面讓出空間。而用來選擇淘汰哪一頁的規則叫做頁面置換算法。

1.1 最佳置換法(OPT)- 理想置換法

       從主存中移出永遠不再需要的頁面;如無這樣的頁面存在,則選擇最長時間不需要訪問的頁面。於所選擇的被淘汰頁面將是以後永不使用的,或者是在最長時間內不再被訪問的頁面,這樣可以保證獲得最低的缺頁率。 即被淘汰頁面是以後永不使用或最長時間內不再訪問的頁面。

1.2 先進先出置換算法(FIFO)

      是最簡單的頁面置換算法。這種算法的基本思想是:當需要淘汰一個頁面時,總是選擇駐留主存時間最長的頁面進行淘汰,即先進入主存的頁面先淘汰。其理由是:最早調入主存的頁面不再被使用的可能性最大。

1.3 時鐘(CLOCK)算法

  • 當某一頁首次裝入主存時,該幀的使用位設置爲1;
  • 當該頁隨後再被訪問到時,它的使用位也被置爲1。
  • 對於頁替換算法,用於替換的候選幀集合看做一個循環緩衝區,並且有一個指針與之相關聯。
  • 當某一頁被替換時,該指針被設置成指向緩衝區中的下一幀。
  • 當需要替換一頁時,操作系統掃描緩衝區,以查找使用位被置爲0的一幀。
  • 每當遇到一個使用位爲1的幀時,操作系統就將該位重新置爲0;
  • 如果在這個過程開始時,緩衝區中所有幀的使用位均爲0,則選擇遇到的第一個幀替換;
  • 如果所有幀的使用位均爲1,則指針在緩衝區中完整地循環一週,把所有使用位都置爲0,並且停留在最初的位置上,替換該幀中的頁。
  • 由於該算法循環地檢查各頁面的情況,故稱爲 CLOCK 算法,又稱爲最近未用( Not Recently Used, NRU )算法。

1.4 最近最久未使用(LRU算法)

      利用局部性原理,根據一個作業在執行過程中過去的頁面訪問歷史來推測未來的行爲。它認爲過去一段時間裏不曾被訪問過的頁面,在最近的將來可能也不會再被訪問。所以,這種算法的實質是:當需要淘汰一個頁面時,總是選擇在最近一段時間內最久不用的頁面予以淘汰。

二. LRU的實現原理

       根據LRU特性可知,LRU算法需要添加頭節點,刪除尾節點。可以用散列表存儲節點,獲取節點的時間度將會降低爲O(1),使用雙向和鏈表作爲數據緩存容器。

LRU.png

三.使用Go實現LRU算法 

       LRU 通常使用hash map + doubly linked list實現。在Golange中很簡單,使用List保存數據,Map來做快速訪問即可。具體實現了下面的函數:

func NewLRUCache(cap int)(*LRUCache)
func (lru *LRUCache)Set(k,v interface{})(error)
func (lru *LRUCache)Get(k interface{})(v interface{},ret bool,err error)
func (lru *LRUCache)Remove(k interface{})(bool)

源碼:

package LRUCache

import (
	"container/list"
	"encoding/json"
	"errors"
	"fmt"
)

type CacheNode struct {
	Key interface{}
	Value interface{}
}

func newCacheNode(key, value interface{}) *CacheNode {
	return &CacheNode{
		Key:key,
		Value:value,
	}
}

var ListDefaultCap int = 512
type LruCache struct {
	cacheCap int
	cacheList* list.List
	cacheMap map[interface{}] *list.Element
}
func NewLRUCache(cap int) *LruCache {
	return &LruCache{
		cacheCap: cap,
		cacheList: list.New(),
		cacheMap: make(map[interface{}] *list.Element),
	}
}

func (lruCache *LruCache)ListSize() int {
	return lruCache.cacheList.Len()
}

func (lruCache *LruCache)Set(key, value interface{}) (*LruCache, error) {
	if lruCache.cacheList == nil {
		return NewLRUCache(ListDefaultCap), errors.New("LRU structure is not initialized, use default settings")
	}

	//map hit, the node is promoted to the top of the list.
	if element, ok := lruCache.cacheMap[key]; ok {
		lruCache.cacheList.Remove(element)
		lruCache.cacheList.PushFront(newCacheNode(key, value))
		//lruCache.cacheMap[key] = lruCache.cacheList.Front()
		return nil, nil
	}
	//Node is inserted into the head node of the linked list.
	newElement := lruCache.cacheList.PushFront(newCacheNode(key, value))
	lruCache.cacheMap[key] = newElement

	//delete the back node
	if lruCache.cacheList.Len() > lruCache.cacheCap {
		cacheNode := lruCache.cacheList.Back().Value.(*CacheNode)
		lruCache.cacheList.Remove(lruCache.cacheList.Back())
		delete(lruCache.cacheMap, cacheNode.Key)
	}
	return nil, nil
}

func (lruCache *LruCache) Get(key interface{}) (value interface{}, err error) {
	if lruCache.cacheList.Len() == 0 {
		return nil, errors.New("LRU structure is not initialized")
	}

	if element, ok := lruCache.cacheMap[key]; ok {
		lruCache.cacheList.MoveToFront(element)
		return element.Value.(*CacheNode).Value, nil
	}
	return nil, errors.New("lruCache.cacheMap is not find the key")
}

func (lruCache *LruCache) Remove(key interface{}) bool {
	if lruCache.cacheList.Len() == 0 {
		return false
	}
	if element, ok := lruCache.cacheMap[key]; ok {
		cacheNode := element.Value.(*CacheNode)
		delete(lruCache.cacheMap, cacheNode.Key)
		lruCache.cacheList.Remove(element)
		return true
	}
	return false
}

func (lruCache *LruCache)TraverShow() {
	if lruCache.cacheList.Len() == 0 {
		return
	}

	for k := range lruCache.cacheMap {
		fmt.Println(k)
	}
	fmt.Println("lruCache.cacheMap.end")
	for k := lruCache.cacheList.Front(); k != nil;  k = k.Next() {
		bytes, _:= json.Marshal(*k)
		fmt.Println(string(bytes))
	}

}

測試:

package main

import (
	"./LRUCache"
	"fmt"
)
func main() {
	lruCache := LRUCache.NewLRUCache(4)
	lruCache.Set(1, "1")
	lruCache.Set(2, "2")
	lruCache.Set(3, "3")
	lruCache.TraverShow()
	fmt.Println("-----------------")
	lruCache.Set(2, "3")
	lruCache.TraverShow()
	fmt.Println("-----------------")
	lruCache.Set(5, "5")
	lruCache.TraverShow()
	fmt.Println("-----------------")
	lruCache.Set(6, "6")
	lruCache.TraverShow()
	fmt.Println("Lru size:", lruCache.ListSize())
	value, err := lruCache.Get(6)
	if err == nil {
		fmt.Println("Ger[6]:", value)
	}

	if lruCache.Remove(3) {
		fmt.Println("lruCache.Remove 3", "true")
	} else {
		fmt.Println("lruCache.Remove 3", "false")
	}
	lruCache.TraverShow()
	fmt.Println("-----------------")

	fmt.Println("lru size", lruCache.ListSize())
	return
}

輸出:

1
2
3
lruCache.cacheMap.end
{"Value":{"Key":3,"Value":"3"}}
{"Value":{"Key":2,"Value":"2"}}
{"Value":{"Key":1,"Value":"1"}}
-----------------
1
2
3
lruCache.cacheMap.end
{"Value":{"Key":2,"Value":"3"}}
{"Value":{"Key":3,"Value":"3"}}
{"Value":{"Key":1,"Value":"1"}}
-----------------
1
2
3
5
lruCache.cacheMap.end
{"Value":{"Key":5,"Value":"5"}}
{"Value":{"Key":2,"Value":"3"}}
{"Value":{"Key":3,"Value":"3"}}
{"Value":{"Key":1,"Value":"1"}}
-----------------
2
3
5
6
lruCache.cacheMap.end
{"Value":{"Key":6,"Value":"6"}}
{"Value":{"Key":5,"Value":"5"}}
{"Value":{"Key":2,"Value":"3"}}
{"Value":{"Key":3,"Value":"3"}}
Lru size: 4
Ger[6]: 6
lruCache.Remove 3 true
2
5
6
lruCache.cacheMap.end
{"Value":{"Key":6,"Value":"6"}}
{"Value":{"Key":5,"Value":"5"}}
{"Value":{"Key":2,"Value":"3"}}
-----------------
lru size 3

Process finished with exit code 0

三.InnoDB中的LRU

       通常來說,數據庫中的緩衝池是通過LRU來管理的。最頻繁使用的頁在LRU列表的前端,最少使用的頁在LRU列表的尾端。當緩衝池不能存放新讀取的頁時。將首先釋放LRU列表中尾端的頁。

       在InnoDB存儲引擎中,緩衝池中頁的大小默認是16KB,同樣使用LRU算法對緩衝池進行管理,稍有不同的是InnoDB存儲引擎對傳統的LRU做了一些優化。在InnoDB中LRU列表還加入了midpoint位置,新讀到的頁,雖然是最新訪問的頁,但並不是直接插入LRU列表的首部,而是放入LRU列表的midpoint位置。默認情況下,該位置在LRU列表長度的5/8處。midpoint可以由參數innodb_old_blocks_pct控制,如:

mysql>SHOW VARIABLES LIKE 'innodb_old_blocks_pct'\G;
*****************************************1.row**********************************
Variable_name:innodb_old_blocks_pct
        Value:37
1 row in set (0.00 sec)

       從上面的例子可以看到,參數innodb_old_blocks_pct默認爲37,表示新讀取的頁插入到LRU列表的37%的位置(差不多3/8的位置)。爲什麼不採用普通的LRU算法,直接將讀取的頁放在首部呢?這是因爲若直接將讀放入到LRU首部,那麼某些SQL操作可能會使緩衝池中頁被刷出,從而影響緩衝池效率。InnoDB有很多掃表的常見操作,這需要訪問表中很多的頁甚至是全部的頁,而這些頁通常來說僅僅是這次的查詢操作需要並不是熱點數據,如果放在首部,非常可能將蘇需要的熱點數據頁從LRU列表中移除。爲了解決這一問題,InnoDB存儲引擎引入另一個參數來進一步管理LRU列表。這個參數是innodb_old_blocks_time,用於表示頁讀取到mid位置後需要等待多久纔會被加入到LRU列表熱端:

mysql>SET GLOBAL innodb_old_blocks_time=1000;
Query OK, 0 rows affected (0.00 sec)

# data or index scan operation
.....

mysql>SET GLOBAL innodb_old_blocks_time=0;
Query OK, 0 rows affected (0.00 sec)   

四.LRU在Redis中的應用

    每次執行命令的時候,redis都會調用freeMemoryIfNeeded:

int freeMemoryIfNeeded(void) {
    size_t mem_reported, mem_used, mem_tofree, mem_freed;
    mstime_t latency, eviction_latency;
    long long delta;
    int slaves = listLength(server.slaves);

   //當客戶端暫停期間,不執行淘汰策略
    if (clientsArePaused()) return C_OK;

    //當內存佔用沒有超出限制,不執行淘汰限制
    mem_reported = zmalloc_used_memory();
    if (mem_reported <= server.maxmemory) return C_OK;

    //內存佔用空間減去輸出緩衝區和AOF緩衝區所需要的大小
    mem_used = mem_reported;
    size_t overhead = freeMemoryGetNotCountedMemory();
    mem_used = (mem_used > overhead) ? mem_used-overhead : 0;

    //如果內存佔用仍然小於限制大小,不執行淘汰
    if (mem_used <= server.maxmemory) return C_OK;

    //計算要釋放的內存大小
    mem_tofree = mem_used - server.maxmemory;
    mem_freed = 0;
    //如果現在的策略是不釋放內存,跳轉到下面的cant_free部分
    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
        goto cant_free; 

    //記錄時間
    latencyStartMonitor(latency);

    while (mem_freed < mem_tofree) {
        int j, k, i, keys_freed = 0;
        static int next_db = 0;
        sds bestkey = NULL;
        int bestdbid;
        redisDb *db;
        dict *dict;
        dictEntry *de;

        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
        {
            struct evictionPoolEntry *pool = EvictionPoolLRU;

            while(bestkey == NULL) {
                unsigned long total_keys = 0, keys;

                //從DB中填充淘汰池的keys,將淘汰策略從DB轉移到淘汰池
                for (i = 0; i < server.dbnum; i++) {
                    db = server.db+i;
                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
                            db->dict : db->expires;
                //計算淘汰池中的數據
                    if ((keys = dictSize(dict)) != 0) {
                        //使用LRU策略從淘汰池中抽取數據到pool
                        evictionPoolPopulate(i, dict, db->dict, pool);
                        total_keys += keys;
                    }
                }

                //沒有淘汰數據就跳出循環
                if (!total_keys) break; 

                /* Go backward from best to worst element to evict. */
                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
                    if (pool[k].key == NULL) continue;
                    bestdbid = pool[k].dbid;

                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
                        de = dictFind(server.db[pool[k].dbid].dict,
                            pool[k].key);
                    } else {
                        de = dictFind(server.db[pool[k].dbid].expires,
                            pool[k].key);
                    }

                    //刪除key中數據
                    if (pool[k].key != pool[k].cached)
                        sdsfree(pool[k].key);
                    pool[k].key = NULL;
                    pool[k].idle = 0;

                    /* If the key exists, is our pick. Otherwise it is
                     * a ghost and we need to try the next element. */
                    if (de) {
                        bestkey = dictGetKey(de);
                        break;
                    } else {
                        /* Ghost... Iterate again. */
                    }
                }
            }
        }

        //隨機策略是經常使用數據的隨機和所有數據的隨機
        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
        {
            //從每個DB中隨機選取key
            for (i = 0; i < server.dbnum; i++) {
                j = (++next_db) % server.dbnum;
                db = server.db+j;
                dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
                        db->dict : db->expires;
                if (dictSize(dict) != 0) {
                    de = dictGetRandomKey(dict);
                    bestkey = dictGetKey(de);
                    bestdbid = j;
                    break;
                }
            }
        }

        //刪除已經選中的key
        if (bestkey) {
            db = server.db+bestdbid;
            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
            //當釋放的內存開始足夠大時,我們可能會在這裏開始花費太多時間,以致無法足夠快地將數據                 傳輸到從屬設備,因此我們在循環內部強制進行傳輸。
            delta = (long long) zmalloc_used_memory();
            latencyStartMonitor(eviction_latency);
            if (server.lazyfree_lazy_eviction)
                dbAsyncDelete(db,keyobj);
            else
                dbSyncDelete(db,keyobj);
            latencyEndMonitor(eviction_latency);
            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
            latencyRemoveNestedEvent(latency,eviction_latency);
            delta -= (long long) zmalloc_used_memory();
            mem_freed += delta;
            server.stat_evictedkeys++;
            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
                keyobj, db->id);
            decrRefCount(keyobj);
            keys_freed++;

            /* When the memory to free starts to be big enough, we may
             * start spending so much time here that is impossible to
             * deliver data to the slaves fast enough, so we force the
             * transmission here inside the loop. */
            if (slaves) flushSlavesOutputBuffers();

           //釋放key。可以釋放固定的,提前計算的內存。涉及多線程的情況,需要檢查目標是否已經到達目標內存。
            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
                overhead = freeMemoryGetNotCountedMemory();
                mem_used = zmalloc_used_memory();
                mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
                if (mem_used <= server.maxmemory) {
                    mem_freed = mem_tofree;
                }
            }
        }

        if (!keys_freed) {
            latencyEndMonitor(latency);
            latencyAddSampleIfNeeded("eviction-cycle",latency);
            goto cant_free; /* nothing to free... */
        }
    }
    latencyEndMonitor(latency);
    latencyAddSampleIfNeeded("eviction-cycle",latency);
    return C_OK;

cant_free:
    //無法刪除內存,阻塞在這循環等待
    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
            break;
        usleep(1000);
    }
    return C_ERR;
}

    根據MAXMEMORY_FLAG_LRU,可以看出evictionPoolPopulate() 是freeMemoryIfNeeded()的輔助函數,用於填充淘汰池。key的插入是升序的,空閒時間短的在左側,空閒時間大的在右側:

void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    int j, k, count;
    dictEntry *samples[server.maxmemory_samples];
    //從樣本中隨機獲取server.maxmemory_samples個數據,可配置默認是5
    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    for (j = 0; j < count; j++) {
        unsigned long long idle;
        sds key;
        robj *o;
        dictEntry *de;

        de = samples[j];
        key = dictGetKey(de);

        //如果採樣不是主dictionary,則重新提取
        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
            if (sampledict != keydict) de = dictFind(keydict, key);
            o = dictGetVal(de);
        }

        //計算LRU時間
        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
            idle = estimateObjectIdleTime(o);
        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
            /* When we use an LRU policy, we sort the keys by idle time
             * so that we expire keys starting from greater idle time.
             * However when the policy is an LFU one, we have a frequency
             * estimation, and we want to evict keys with lower frequency
             * first. So inside the pool we put objects using the inverted
             * frequency subtracting the actual frequency to the maximum
             * frequency of 255. */
            idle = 255-LFUDecrAndReturn(o);
        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
            /* In this case the sooner the expire the better. */
            idle = ULLONG_MAX - (long)dictGetVal(de);
        } else {
            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
        }

        //將元素插入池中。首先找到第一個空閒時間小於我們的桶
        k = 0;
        while (k < EVPOOL_SIZE &&
               pool[k].key &&
               pool[k].idle < idle) k++;
        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
            //如果沒有空桶則不能插入
            continue;
        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
            //插入到空位置,插入之前無需設置
        } else {
            //插入到中間。
            if (pool[EVPOOL_SIZE-1].key == NULL) {
                //如果最右處有可用空間,在k處插入,k之後的所有元素後移1

                /* Save SDS before overwriting. */
                sds cached = pool[EVPOOL_SIZE-1].cached;
                memmove(pool+k+1,pool+k,
                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
                pool[k].cached = cached;
            } else {
                //刪除第一個元素
                k--;
                //將k左側(包括k)的所有元素向左移動,我們丟棄空閒時間較短的元素
                sds cached = pool[0].cached; //覆蓋前保存SDS
                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
                memmove(pool,pool+1,sizeof(pool[0])*k);
                pool[k].cached = cached;
            }
        }

        /* Try to reuse the cached SDS string allocated in the pool entry,
         * because allocating and deallocating this object is costly
         * (according to the profiler, not my fantasy. Remember:
         * premature optimizbla bla bla bla. */
        int klen = sdslen(key);
        if (klen > EVPOOL_CACHED_SDS_SIZE) {
            pool[k].key = sdsdup(key);
        } else {
            memcpy(pool[k].cached,key,klen+1);
            sdssetlen(pool[k].cached,klen);
            pool[k].key = pool[k].cached;
        }
        pool[k].idle = idle;
        pool[k].dbid = dbid;
    }
}

       Redis使用的是近似LRU算法,它跟常規的LRU算法還不太一樣。近似LRU算法通過隨機採樣法淘汰數據,每次隨機出5(默認)個key,從裏面淘汰掉最近最少使用的key。可以通過maxmemory-samples參數修改採樣數量:例:maxmemory-samples 10 ;maxmenory-samples配置的越大,淘汰的結果越接近於嚴格的LRU算法。Redis爲了實現近似LRU算法,給每個key增加了一個額外增加了一個24bit的字段,用來存儲該key最後一次被訪問的時間。
       Redis3.0中對Redis的LRU進行了優化,採用了近似策略。Redis3.0對近似LRU算法進行了一些優化。新算法會維護一個候選池(大小爲16),池中的數據根據訪問時間進行排序,第一次隨機選取的key都會放入池中。隨後每次隨機選取的key只有在訪問時間小於池中最小的時間纔會放入池中,直到候選池被放滿。當放滿後,如果有新的key需要放入,則將池中最後訪問時間最大(最近被訪問)的移除。當需要淘汰的時候,則直接從池中選取最近訪問時間最小(最久沒被訪問)的key淘汰掉就行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章