一、理论
LRU算法算是个常见的算法,很有必要去了解它,现在我们就来看看什么是 LRU
LRU 的全称是 Least Recently Used(最近最少使用),就如它的含义一样,最近最少使用的。在实际的场景中大多会把它当作一种 淘汰策略
它的应用场景也很简单,我们的存储空间总是有限的,一旦超过了最大限度就必须要淘汰一些数据,那淘汰哪种数据才算是合理的呢?合理的方式有很多,不同的场景也不一样,但淘汰 最近最少使用的数据,在绝大部分场景下都是合理的,所以LRU算法很常见。
二、实践
3-1、Redis 淘汰策略
redis的6大淘汰策略里面就有2种是 LRU策略
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错
3-2、MyBtais 二级缓存
关于MyBatis的缓存可以参看这篇文章 https://blog.csdn.net/Tomwildboar/article/details/127570765
MyBatis的二级缓存各种功能实现是基于 装饰器模式来实现的,默认就是 LRU
下面是MyBatis 的 LruCache源码, 因为比较少就全部粘贴了
package org.apache.ibatis.cache.decorators;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
/**
* Lru (least recently used) cache decorator.
*
* @author Clinton Begin
*/
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
keyMap.get(key); // touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
它里面主要有两个操作
- 对 LinkedHashMap 的 removeEldestEntry 方法进行了重写
- 添加的时候会去调用 cycleKeyList 方法
removeEldestEntry
LinkedHashMap 底层也还是基于HashMap的,并且没有重写 put方法,是直接调用的 HashMap的put方法
在HashMap的put方法里面最后会去调用 afterNodeInsertion 方法, LinkedHashMap 里面重写了这个方法如下, evict 默认就是 true
这个方法的描述 possibly remove eldest ,可能移除最老的(也就是 first),当if判断是ture的时候就会删除这个元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
我们再来看看重写后的 removeEldestEntry ,eldest 通过上面其实就是链表头第一个元素
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
如果当前容器里面的数据大于规定的容量,就执行 eldestKey = eldest.getKey();
cycleKeyList
添加元素的时候会调用这个方法
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
上面我们知道,当元素大于容量规定大小,eldestKey 就会等于第一个元素的key,这时候就会删掉这个元素,从而实现 LRU策略
3-3、MySQL bufferPool
上面这种LRU策略,在某种情况下会存在漏洞,如果我们每次都是淘汰第一个元素,那如果我们在查询到元素A后就一直不再使用它了,这样它从链表尾到链表尾才会被淘汰,但是我们有一个元素B在它的后面但是经常被使用,但元素B依旧比元素A先淘汰,这样就不合理了。
MySQL 操作数据的时候先会把数据加载到内存中(Buffer Pool),但是内存的大小是有限的,所以也就存在了淘汰策略,如果只是简单的使用LRU策略,就会存在上述的问题。
MySQL对LRU策略进行了改造来完善这个问题,完整的buffer pool 参看这里 https://blog.csdn.net/Tomwildboar/article/details/121525187
它的逻辑也很简单
- 把内存分为 冷数据区和热数据区,数据默认都是进入冷数据区
- 冷数据区的数据经过默认的时间间隔被访问就会进入热数据区(默认 1s)
- 热数据区也进行划分,前 1/4的热数据区访问的时候不会进行移动
- 后 3/4的热数据被访问的时候会被提到前 1/4的位置
- 淘汰的时候优先淘汰冷数据区