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淘汰掉就行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值