一、cache应具备的逐出策略(缓存元素剔除策略)
1)数量逐出:通过LRU算法(最近最少使用)可以实现超过limit时顶替掉最近最少使用的元素
2)重量逐出:大小限制(通过softreference实现),一般是对象如果实现Serializable接口,则通过计算对象的字节大小作为重量逐出参考。
3)时间逐出:比如限制cache元素只能存活30秒
二、Guava cache:LoadingCache
1、逐出策略说明
数量逐出:CacheBuilder的maximumSize接口
时间逐出:CacheBuilder的expireAfterAccess、expireAfterWrite接口
- expireAfterAccess:接口中access time会根据Write/Update/Read操作进行改变
- expireAfterWrite:接口中write time会根据Write/Update操作进行改变
重量逐出:CacheBuilder的maximumWeight(以int类型代表所有缓存元素的最大重量)
实体类:Employee.java
package com.mzj.guava.cache.guava;
import com.google.common.base.MoreObjects;
public class Employee {
private String name;
private String dept;
private String empID;
public Employee(String name, String dept, String empID) {
this.name = name;
this.dept = dept;
this.empID = empID;
}
public String getName() {
return name;
}
public String getDept() {
return dept;
}
public String getEmpID() {
return empID;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("dept", dept)
.add("empID", empID)
.toString();
}
}
1、测试数量逐出、重量逐出:CacheLoaderTest.java
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import org.junit.Test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
/**
* +
* 基本用法
* <p>
* 查询到Employee放入cache,下次从cache直接拿
*/
public class CacheLoaderTest {
/**
* 方便进行断言的变量:
*
* true:从数据库获取数据
* false:从缓存获取数据
*/
private boolean isTrue = false;
@Test
public void testBasic() {
LoadingCache<String, Employee> cache = CacheBuilder.newBuilder().maximumSize(10)//缓存数量
.expireAfterAccess(2, TimeUnit.SECONDS)//缓存元素过期时间(30s过期)
.build(new CacheLoader<String, Employee>() {//告诉cache,数据从哪里获取
@Override
public Employee load(String key) throws Exception {
return findEmployeeByName(key);
}
});
try {
Employee employee = cache.get("mazhongjia");
assertThat(employee, notNullValue());
assertLoadFromDBThenReset();//走DB
System.out.println(employee);
employee = cache.get("mazhongjia");
assertLoadFromCache();//走cache
System.out.println(employee);
TimeUnit.SECONDS.sleep(3);//休眠3秒,让cache中元素过期
employee = cache.get("mazhongjia");
assertLoadFromDBThenReset();//走DB
System.out.println(employee);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void testEvictionByWeigh() {
/**
* 定义一个:秤
*/
Weigher<String, Employee> weigher = ((key, employee) -> employee.getName().length() + employee.getEmpID().length() + employee.getDept().length());
LoadingCache<String, Employee> cache = CacheBuilder.newBuilder().maximumWeight(45)//缓存重量(所有缓存元素的最大重量是:45)
.concurrencyLevel(1)// 设置并发级别为1,并发级别是指可以同时写缓存的线程数(划分多个segment)
.weigher(weigher)
.build(new CacheLoader<String, Employee>() {
@Override
public Employee load(String key) throws Exception {
return findEmployeeByName(key);
}
});
cache.getUnchecked("gavin");
assertLoadFromDBThenReset();//走DB
cache.getUnchecked("kevin");
assertLoadFromDBThenReset();//走DB
cache.getUnchecked("allen");
assertLoadFromDBThenReset();//走DB
assertThat(cache.size(),equalTo(3L));//断言缓存元素为3个
assertThat(cache.getIfPresent("gavin"),notNullValue());//getIfPresent:如果存在则返回,如果不存在也不会去DB中拿
cache.getUnchecked("mamam");
assertThat(cache.getIfPresent("kevin"),nullValue());//getIfPresent:如果存在则返回,如果不存在也不会去DB中拿;这里已经访问过gavin了,所以目前kevin是最老的未被使用的
assertThat(cache.size(),equalTo(3L));//断言缓存元素为3个
}
/**
* 断言去DB获取数据
*/
private void assertLoadFromDBThenReset() {
assertThat(true, equalTo(this.isTrue));
this.isTrue = false;
}
/**
* 断言去cache获取数据
*/
private void assertLoadFromCache() {
assertThat(false, equalTo(this.isTrue));
}
/**
* 去数据库中通过名称查询Employee
*
* @param name
* @return
*/
private Employee findEmployeeByName(final String name) {
this.isTrue = true;
return new Employee(name, name, name);
}
}
说明1:CacheBuilder是创建LoadingCache的构建器模式的Builder类
说明2:创建CacheLoader除了直接new以外,还可以如下三种方式:
2、测试时间逐出:CacheLoaderTest2.java
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
public class CacheLoaderTest2 {
/**
* CacheBuilder的expireAfterAccess接口测试
*
* 接口中access time会根据Write/Update/Read操作进行改变
*
* @throws InterruptedException
*/
@Test
public void testEvictionByAccessTime() throws InterruptedException {
LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.build(this.createCacheLoader());
assertThat(cache.getUnchecked("Alex"),notNullValue());
assertThat(cache.size(),equalTo(1L));
TimeUnit.SECONDS.sleep(3);
assertThat(cache.getIfPresent("Alex"),nullValue());
assertThat(cache.getUnchecked("Guava"),notNullValue());
TimeUnit.SECONDS.sleep(1);
assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
TimeUnit.MILLISECONDS.sleep(990);
assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
TimeUnit.SECONDS.sleep(1);
assertThat(cache.getIfPresent("Guava"),notNullValue());//测试Read操作会修改access time
}
/**
* CacheBuilder的expireAfterWrite接口测试
*
* 接口中write time会根据Write/Update操作进行改变
*
* @throws InterruptedException
*/
@Test
public void testEvictionByWriteTime() throws InterruptedException {
LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(this.createCacheLoader());
assertThat(cache.getUnchecked("Alex"),notNullValue());
assertThat(cache.size(),equalTo(1L));
assertThat(cache.getUnchecked("Guava"),notNullValue());
TimeUnit.SECONDS.sleep(1);
assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
TimeUnit.MILLISECONDS.sleep(990);
assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
TimeUnit.SECONDS.sleep(1);
assertThat(cache.getIfPresent("Guava"),nullValue());//测试Read操作不会修改write time
}
private final CacheLoader<String,Employee> createCacheLoader(){
return CacheLoader.from(key -> new Employee(key,key,key));
}
}
3、CacheBuilder的weakKeys()与weakValues()
weakKeys:
weakValues:
也就是构建LoadingCache时如果调用CacheBuilder的上面两个方法,将使得key或者value被wrapped,by WeakReference,会在Minor GC、Major GC和Full GC时释放其占用内存。
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
public class WeakKeyTest {
/**
* 在JVM发生Minor GC、Major GC和Full GC时会将所有weak reference全部释放掉
*/
@Test
public void testWeakKey() throws InterruptedException {
LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(this.createCacheLoader());
assertThat(cache.getUnchecked("Alex"),notNullValue());
assertThat(cache.getUnchecked("Guava"),notNullValue());
/**
* active method
*
* Thread active design pattern
*/
System.gc();
TimeUnit.MILLISECONDS.sleep(100);
assertThat(cache.getIfPresent("Alex"),nullValue());
}
private final CacheLoader<String,Employee> createCacheLoader(){
return CacheLoader.from(key -> new Employee(key,key,key));
}
}
4、CacheBuilder的softValues()
softValues:
也就是构建LoadingCache时如果调用CacheBuilder的上面方法,将使得value被wrapped,by SoftReference,会在JVM在内存快不够时(快要OOM时)有可能~~会尝试~~~将所有soft reference释放掉
因此,测试本示例,需要修改JVM参数:-Xmx128M -Xms64M -XX:+PrintGCDetails
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
public class SoftKeyTest {
/**
* 在JVM在内存快不够时(快要OOM时)有可能~~会尝试~~~将所有soft reference释放掉
*
* 测试本示例,最好修改JVM参数:-Xmx128M -Xms64M -XX:+PrintGCDetails
*/
@Test
public void testSoftValues() throws InterruptedException {
LoadingCache<String, EmployeeBig> cache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.softValues()
.build(this.createCacheLoader());
int i = 0;
for (; ; ) {
cache.put("Alex" + i, new EmployeeBig("Alex" + 1, "Alex" + 1, "Alex" + 1));
System.out.println("The Empolyee [" + (i++) + "] is store into cache.");
TimeUnit.MILLISECONDS.sleep(600);
}
}
private final CacheLoader<String, EmployeeBig> createCacheLoader() {
return CacheLoader.from(key -> new EmployeeBig(key, key, key));
}
}
CacheBuilder无softKeys()方法
2、cache中空元素的处理
注意,这里的空元素是通过CacheLoader的load方法返回的null,而不是缓存过期变为null
没有经过任何处理时cacheLoader的load方法如果返回null值,会抛出异常
@Test
public void testLoadNullValue() {
/**
* cacheLoader的load方法如果返回null值,会抛出异常
*/
CacheLoader<String, Employee> cacheLoader = CacheLoader.from(k -> k.equals("null") ? null : new Employee(k, k, k));
LoadingCache<String,Employee> cache = CacheBuilder.newBuilder().build(cacheLoader);
Employee alax = cache.getUnchecked("Alex");
assertThat(alax,notNullValue());
assertThat(alax.getName(),equalTo("Alex"));
try{
Employee employeeNull = cache.getUnchecked("null");//此行会抛出异常,由于cacheLoader的load方法由于参数"null"而返回null
assertThat(employeeNull,nullValue());
fail("should not process to here.");
}catch (Exception e){
e.printStackTrace();
assertThat(e instanceof CacheLoader.InvalidCacheLoadException,equalTo(true));
}
}
通过使用guava的Optional包装了缓存中元素,使得cacheLoader的load方法不可能返回null,而解决了上面测试用例中异常
@Test
public void testLoadNullValueUseOptional(){
CacheLoader<String, Optional<Employee>> loader = new CacheLoader<String, Optional<Employee>>() {
@Override
public Optional<Employee> load(String key) throws Exception {
if (key.equals("null"))
return Optional.fromNullable(null);
else
return Optional.fromNullable(new Employee(key,key,key));
}
};
LoadingCache<String,Optional<Employee>> cache = CacheBuilder.newBuilder().build(loader);
Optional<Employee> alax = cache.getUnchecked("Alex");
assertThat(alax.get(),notNullValue());
/**
* 1、此行断言体现了从cache获取到到Optional为Absent的实现(代表空的Optional)但是并未返回null
*/
assertThat(cache.getUnchecked("null").orNull(),nullValue());//不能调用cache.getUnchecked("null")的get会抛异常(返回Absent的实现)
/**
* 2、此行断言体现了更进一步,如果为”null的Optional,则使用默认值
*
* 好处是不再需要进行判断:从cache中获取的内容是否为null,没有则返回默认值
*/
Employee def = cache.getUnchecked("null").or(new Employee("default","default","default"));
assertThat(def,notNullValue());
}
3、cache的refresh
作用:在缓存对应DB中数据被修改后,需要将缓存数据逐出
方案一:修改完DB数据后,令缓存中数据实效,下次再请求时重新写入缓存(此方案适用于对数据要求精确的场景)
方案二:让缓存数据定期过期,比如固定2秒就过期(此方案适用于对数据要求不精确的场景,比如评论等)
guava中的cache的refresh只是方案二的实现
CacheBuilder的refreshAfterWrite方法:
设置写入后刷新时间(刷新个人理解为过期)
实现上,guava并不是内部有一个线程不断检查过期元素,而是只记录这个的刷新时间,请求时判断,如果超过这个时间,则不去缓存中拿
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
public class CacheLoaderTest4 {
@Test
public void testCacheeRefresh() throws InterruptedException {
CacheLoader<String, Long> cacheLoader = CacheLoader.from(k -> System.currentTimeMillis());
LoadingCache<String,Long> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(2,TimeUnit.SECONDS)//设置写入后刷新时间(刷新个人理解为过期),如果把这个设置注释掉,会测试失败,实现上,guava并不是内部有一个线程不断检查过期元素,而是只记录这个的刷新时间,请求时判断,如果超过这个时间,则不去缓存中拿
.build(cacheLoader);
Long result1 = cache.getUnchecked("Alex");
TimeUnit.SECONDS.sleep(3);
Long result2 = cache.getUnchecked("Alex");
assertThat(result1.longValue() != result2.longValue(),equalTo(true));
}
}
4、cache的preload(预加载)
作用:cache启动时,加载某些元素入缓存,但是机制存在一个缺陷:预加载的缓存元素与缓存本身指定的规则如果使用不当,会出现不一致的地方,需要注意。
package com.mzj.guava.cache.guava;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* cache的preload(预加载)
*/
public class CacheLoaderTest5 {
/**
* 方案:
*
* 此方案存在问题:在创建CacheLoader时,限制(规定)了cache的value时key的uppercase
*
* 但是预加载的时候,可以不按照此规则向cache中放入数据
*
* 在进行业务逻辑处理时依然按照这个规则
*
* @throws InterruptedException
*/
@Test
public void testCachePreLoad() throws InterruptedException {
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);//这里限制(规定)了cache的value时key的uppercase
LoadingCache<String,String> cache = CacheBuilder.newBuilder()
.build(cacheLoader);
Map<String,String> preData = new HashMap<String,String>(){
{
put("alex","ALEX");
put("mazhongjia","MAZHONGJIA");
put("hello","hello");//没有按照规则向缓存放入数据
}
};
cache.putAll(preData);//预加载
assertThat(cache.size(),equalTo(2L));
assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));
}
}
5、Removal通知
作用:缓存数据在删除时,进行通知
自定义RemovalListener并加入CacheBuilder
package com.mzj.guava.cache.guava;
import com.google.common.cache.*;
import org.hamcrest.core.Is;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* cache的removal通知(缓存中元素删除后通知)
*/
public class CacheLoaderTest6 {
@Test
public void testCacheRemovedNotification() throws InterruptedException {
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);//这里限制(规定)了cache的value时key的uppercase
RemovalListener<String, String> listener = notification -> {
//如果是被逐出的
if (notification.wasEvicted()) {
/**
* RemovalCause:逐出原因枚举
* EXPLICIT:手工逐出(wasEvicted方法返回false,不会进入此if判断)
* REPLACED:被替换了(wasEvicted方法返回false,不会进入此if判断)
* COLLECTED:垃圾回收,GC的时候被逐出
* EXPIRED:时间过期,被逐出
* SIZE:数量逐出
*/
RemovalCause removalCause = notification.getCause();
assertThat(removalCause, Is.is(RemovalCause.SIZE));
assertThat(notification.getKey(), equalTo("Alex"));
}
};
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)//设置缓存数量为3
.removalListener(listener)//添加remove listener
.build(cacheLoader);
cache.getUnchecked("Alex");
cache.getUnchecked("mazhongjia");
cache.getUnchecked("huna");
cache.getUnchecked("maxiaotang");
}
}
6、CacheStats
1、作用:统计,cache的命中率、命中数、miss率、miss数量
package com.mzj.guava.cache.guava;
import com.google.common.cache.*;
import org.hamcrest.core.Is;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* cache的stat
*/
public class CacheLoaderTest7 {
@Test
public void testCacheStat() throws InterruptedException {
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.recordStats()//通过构造CacheBuilder时调用recordStats还开启记录CacheStats
.build(cacheLoader);
assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));
/**
* 获取cache的状态对象,CacheStats是不可变对象,把CacheStats设计成final的好处是:如果有多个线程访问cache,不同线程获取CacheStats之间互不影响
*/
CacheStats stats = cache.stats();
//目前还没有请求从cache中获取到数据
assertThat(stats.hitCount(),equalTo(0L));//命中数
assertThat(stats.missCount(),equalTo(1L));//miss数
assertThat(stats.hitRate(),equalTo(0.0D));//命中率
assertThat(stats.missRate(),equalTo(1.0D));//miss率
}
}
7、CacheBuilderSpec
1、作用:之前小节创建LoadingCache的方式都是通过CacheBuilder在代码中创建,以构造器的方式设置各种属性,除此之外,也可通过配置文件的方式创建,这种创建方式常用在spring ioc容器中创建时使用。
原理:实际上,CacheBuilderSpec就是通过规定的一个字符串格式(属性名1=属性值1,属性名2=属性值2)解析得到各部分属性、值来创建CacheBuilder。这样就可以将这个属性字符串在外部进行配置了。
package com.mzj.guava.cache.guava;
import com.google.common.cache.*;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* cache的CacheBuilderSpec
*/
public class CacheLoaderTest8 {
@Test
public void testacheBuilderSpec() throws InterruptedException {
String spec = "maximumSize=5,recordStats";
CacheBuilderSpec cacheBuilderSpec = CacheBuilderSpec.parse(spec);
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
LoadingCache<String, String> cache = CacheBuilder.from(cacheBuilderSpec)
.build(cacheLoader);
assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));
CacheStats stats = cache.stats();
//目前还没有请求从cache中获取到数据
assertThat(stats.hitCount(),equalTo(0L));//命中数
assertThat(stats.missCount(),equalTo(1L));//miss数
assertThat(stats.hitRate(),equalTo(0.0D));//命中率
assertThat(stats.missRate(),equalTo(1.0D));//miss率
}
}
完整代码示例:
https://github.com/mazhongjia/googleguava/tree/master/src/main/java/com/mzj/guava/cache/guava