一.頁面置換算法
地址映射過程中,若在頁面中發現所要訪問的頁面不在內存中,則產生缺頁中斷。當發生缺頁中斷時,如果操作系統內存中沒有空閒頁面,則操作系統必須在內存選擇一個頁面將其移出內存,以便爲即將調入的頁面讓出空間。而用來選擇淘汰哪一頁的規則叫做頁面置換算法。
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),使用雙向和鏈表作爲數據緩存容器。
三.使用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淘汰掉就行。