缓存回收策略
1.基于空间
设置缓存的存储空间,超过容量限制按照一定的规则去移除。
2.基于容量
当缓存条数超过规定容量按照一定的规则去移除旧数据。
3.基于时间(存活时间空闲时间)
TTl(Time To Live):存活期,缓存数据最后更新时间到指定时间如果没有更新或创建则进行过期清除。 TTI(Time To Idle):空闲期,当缓存数据多久没有访问就从缓存中移除。 4.基于对象的引用 软引用:当JVM内存不足时,垃圾回收会回收掉这些软引用,腾出空间避免OOM。 弱引用:当进行垃圾回收的时候会被回收掉。(注意:只有在没有强应用去引用它的时候才会被回收)
public static void testReference() {
School so = new School();
WeakReference s = new WeakReference(new Student("z"));
//通过强引用去引用它,通过注释这行可以观察出有没有强引用去引用它时候的变化
so.students.add(s.get());
//手动触发gc
System.gc();
System.out.println(s.get());
}
class Student {
private String name;
public Student(String name) {
this.name = name;
}
}
class School {
public List students = new ArrayList();
}
缓存回收算法
使用基于空间和基于容量的会使用一定的策略移除旧数据,常见的如下。
FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除。
LRU(Least Recently Used):最近最少使用算法,使用时间距离现在最久的那个被移除。
LFU(Least Frequently Used):最不常用算法,一定时间段内使用次数(频率)最少的那个被移除。
实际应用中基于LRU的缓存居多,如Guava Cache、Ehcache支持LRU。 ##缓存类型
堆缓存:使用Java堆内存来存储缓存对象。使用堆缓存的好处是没有序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很大时, GC暂停时间会变长,存储容量受限于堆空间大小。一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。可以使用Guava Cache、Ehcache 3.x、MapDB实现。
堆外缓存:即缓存数据存储在堆外内存,可以减少GC暂停时间(堆对象转移到堆外,GC扫描和移动的对象变少了),可以支持更大的缓存空间(只受机器内存大小限制,不受堆空间的影响)。但是,读取数据时需要序列化/反序列化,因此,会比堆缓存慢很多。可以使用Ehcache 3.x、MapDB实现。
磁盘缓存:即缓存数据的存储在磁盘上,当JVM重启时数据还是在的。而堆缓存/堆外缓存重启时数据会丢失,需要重新加载。可以使用Ehcache 3.x、MapDB实现。
分布式缓存:上文提到的缓存是进程内缓存和磁盘缓存,在多JVM实例的情况时,会存在两个问题:1.单机容量问题;2.数据一致性问题(多台JVM实例的缓存数据不一致怎么办),不过,这个问题不用太纠结,既然数据允许缓存,则表示允许一定时间内的不一致,因此,可以设置缓存数据的过期时间来定期更新数据;3.缓存不命中时,需要回源到DB/服务查询变多:每个实例在缓存不命中情况下都会回源到DB加载数据,因此,多实例后DB整体的访问量就变多了,解决办法可以使用如一致性哈希分片算法来解决。因此,这些情况可以考虑使用分布式缓存来解决。可以使用ehcache-clustered(配合Terracotta server)实现Java进程间分布式缓存。当然也可以使用如Redis实现分布式缓存。
我们根据各自缓存的特点可以设置为多级缓存。比如可以将堆缓存设置为1级缓存,存储一些热数据。将堆外或者磁盘设置为二级缓存,将分布式缓存集群设置为三级缓存。后面文章再写各自缓存的具体示例。