LRU这是一种内存淘汰算法,实际上解释它也挺简单的,分为两个维度:1.容量;2.时间。
我们有一个有界队列,如果不断地往其中写入数据,总会满的,这个时候需要对数据进行淘汰。如果超过了容量,在插入数据的时候应该淘汰哪些数据。那就是不经常用的,冷数据。LRU的主旨是保存最近经常使用的数据,经常使用的数据叫做热点数据,保留它们的价值在于:它们经常被访问,从缓存中取就不用去频繁访问数据库等其他存储了,这样就提高了程序效率。
一般的缓存容器都是双端队列型的,有head和tail。容量达到上限插入的时候,tail的元素会出队。如果队中元素经常被访问,那它就会被提到head端去。JDK提供了一种根据容量来出队的数据结构:LinkedHashMap。
它的结构是HashMap的结构,在这基础上,它的每个链表对象Entity,多加入了pre和next指针(大概意思),用来表示插入顺序的。所以它能保证访问时候走的是Hash的体系O(1),而在遍历的时候可以识别插入顺序;与此同时,它还可以实现LRU。
public class LRULinkedMap<K,V> extends LinkedHashMap<K,V>{ private static final int DEFAULT_CAPACITY = 1<<4; private static final float DEFAULT_FACTOR = 0.75f; private static final int DEFAULT_LIMIT = DEFAULT_CAPACITY/2; private int limit; public LRULinkedMap(){ super(DEFAULT_CAPACITY,DEFAULT_FACTOR,true); limit = DEFAULT_LIMIT; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { if(this.size() > limit){ System.out.println("移除最老的节点:"+eldest.getKey()+","+eldest.getValue()); return true; }else { return false; } } }
重写它的removeEldestEntity方法告诉它什么时候将最老的节点移除掉就好。这里有两个不好的地方,一是它线程不安全;二是它只能根据容量移除,如果要根据时间的话你得在放对象的时候对象上带有时间戳,自己实现。
LRULinkedMap<Integer,String> lruLinkedMap = new LRULinkedMap(); for(int i=0;i<20;i++){ String val = "序号:"+i; System.out.println("添加节点:"+i+","+val); lruLinkedMap.put(i,val); }
添加节点:0,序号:0 添加节点:1,序号:1 添加节点:2,序号:2 添加节点:3,序号:3 添加节点:4,序号:4 添加节点:5,序号:5 添加节点:6,序号:6 添加节点:7,序号:7 添加节点:8,序号:8 移除最老的节点:0,序号:0 添加节点:9,序号:9 移除最老的节点:1,序号:1 添加节点:10,序号:10 移除最老的节点:2,序号:2 添加节点:11,序号:11 移除最老的节点:3,序号:3 添加节点:12,序号:12 移除最老的节点:4,序号:4 添加节点:13,序号:13 移除最老的节点:5,序号:5 添加节点:14,序号:14 移除最老的节点:6,序号:6 添加节点:15,序号:15 移除最老的节点:7,序号:7 添加节点:16,序号:16 移除最老的节点:8,序号:8 添加节点:17,序号:17 移除最老的节点:9,序号:9 添加节点:18,序号:18 移除最老的节点:10,序号:10 添加节点:19,序号:19 移除最老的节点:11,序号:11
google提供了我们一种JVM缓存的工具
<!--jvm本地缓存--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>24.0-jre</version> </dependency>
Cache<Integer,String> cache = CacheBuilder.newBuilder() .maximumSize(1<<4) .expireAfterWrite(3,TimeUnit.SECONDS) .build(); cache.put(1,"序号1"); System.out.println("立即获取:"+cache.getIfPresent(1)); Thread.sleep(5000); System.out.println("超时获取:"+cache.getIfPresent(1));
立即获取:序号1 超时获取:null
一个简单的构建就能使用JVM缓存,本地缓存的优势在于:节省了网络调用(Redis,Memcached)、数据库访问、RPC调用。
它的put方法是这样的,很熟悉,就是1.7的hashmap分段锁,所以它又是线程安全的。
public V put(K key, V value) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(value); int hash = this.hash(key); return this.segmentFor(hash).put(key, hash, value, false); }
此外它还提供了LoadingCache,什么概念呢,失效的Key它会调用你重写的方法重新载入,也就是带刷新数据的功能。
缓存的使用主旨,应该在动静分离上,对于一些不怎么变化的数据,或者有时效性又经常访问的数据,使用缓存防止磁盘IO,是一个提升系统性能的方案。