一.页面置换算法
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
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淘汰掉就行。