闽公网安备 35020302035485号
4.监控缓存加载/命中情况
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class GuavaCacheService {
public void setCache() {
LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
//设置写缓存后n秒钟过期
.expireAfterWrite(60, TimeUnit.SECONDS)
//设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
//只阻塞当前数据加载线程,其他线程返回旧值
//.refreshAfterWrite(13, TimeUnit.SECONDS)
//设置缓存的移除通知
.removalListener(notification -> {
System.out.println(notification.getKey() + " " + notification.getValue() + " 被移除,原因:" + notification.getCause());
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new DemoCacheLoader());
//模拟线程并发
new Thread(() -> {
//非线程安全的时间格式化工具
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
try {
for (int i = 0; i < 10; i++) {
String value = cache.get(1);
System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
TimeUnit.SECONDS.sleep(3);
}
} catch (Exception ignored) {
}
}).start();
new Thread(() -> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
try {
for (int i = 0; i < 10; i++) {
String value = cache.get(1);
System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
TimeUnit.SECONDS.sleep(5);
}
} catch (Exception ignored) {
}
}).start();
//缓存状态查看
System.out.println(cache.stats().toString());
}
/**
* 堆代码 duidaima.com
* 随机缓存加载,实际使用时应实现业务的缓存加载逻辑,例如从数据库获取数据
*/
public static class DemoCacheLoader extends CacheLoader<Integer, String> {
@Override
public String load(Integer key) throws Exception {
System.out.println(Thread.currentThread().getName() + " 加载数据开始");
TimeUnit.SECONDS.sleep(8);
Random random = new Random();
System.out.println(Thread.currentThread().getName() + " 加载数据结束");
return "value:" + random.nextInt(10000);
}
}
}
LoadingCache是Cache的子接口,相比较于Cache,当从LoadingCache中读取一个指定key的记录时,如果该记录不存在,则LoadingCache可以自动执行加载数据到缓存的操作。CacheBuilder.newBuilder() // 设置并发级别为cpu核心数 .concurrencyLevel(Runtime.getRuntime().availableProcessors()) .build();缓存的初始容量设置
CacheBuilder.newBuilder() // 设置初始容量为100 .initialCapacity(100) .build();设置最大存储
基于权重的清除: 使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights)。
.refreshAfterWrite 写入数据后多久过期,只阻塞当前数据加载线程,其他线程返回旧值
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class GuavaCacheService {
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (true) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " size: " + cache.size());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}).start();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
cache.put(1, "a");
System.out.println("写入 key:1 ,value:" + cache.getIfPresent(1));
Thread.sleep(10000);
cache.put(2, "b");
System.out.println("写入 key:2 ,value:" + cache.getIfPresent(2));
Thread.sleep(10000);
System.out.println(sdf.format(new Date())
+ " sleep 10s , key:1 ,value:" + cache.getIfPresent(1));
System.out.println(sdf.format(new Date())
+ " sleep 10s, key:2 ,value:" + cache.getIfPresent(2));
}
}
部分输出结果:23:57:36 size: 0
写入 key:1 ,value:a
23:57:38 size: 1
23:57:40 size: 1
23:57:42 size: 1
23:57:44 size: 1
23:57:46 size: 1
写入 key:2 ,value:b
23:57:48 size: 1
23:57:50 size: 1
23:57:52 size: 1
23:57:54 size: 1
23:57:56 size: 1
23:57:56 sleep 10s , key:1 ,value:null
23:57:56 sleep 10s, key:2 ,value:null
23:57:58 size: 0
23:58:00 size: 0
23:58:02 size: 0
...
...
上面程序设置了缓存过期时间为5S,每打印一次当前的size需要2S,打印了5次size之后写入key 2,此时的size为1,说明在这个时候才把第一次应该过期的key 1给删除。RemovalListener<String, String> listener = notification -> System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(5)
.removalListener(listener)
.build();
但是要注意的是:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
public class GuavaCacheService {
private static Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.build();
public static void main(String[] args) {
new Thread(() -> {
System.out.println("thread1");
try {
String value = cache.get("key", new Callable<String>() {
public String call() throws Exception {
System.out.println("thread1"); //加载数据线程执行标志
Thread.sleep(1000); //模拟加载时间
return "thread1";
}
});
System.out.println("thread1 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("thread2");
try {
String value = cache.get("key", new Callable<String>() {
public String call() throws Exception {
System.out.println("thread2"); //加载数据线程执行标志
Thread.sleep(1000); //模拟加载时间
return "thread2";
}
});
System.out.println("thread2 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}
}).start();
}
}
输出结果为:thread1 thread2 thread2 thread1 thread2 thread2 thread2可以看到输出结果:两个线程都启动,输出thread1,thread2,接着又输出了thread2,说明进入了thread2的call方法了,此时thread1正在阻塞,等待key被设置。然后thread1 得到了value是thread2,thread2的结果自然也是thread2。这段代码中有两个线程共享同一个Cache对象,两个线程同时调用get方法获取同一个key对应的记录。由于key对应的记录不存在,所以两个线程都在get方法处阻塞。此处在call方法中调用Thread.sleep(1000)模拟程序从外存加载数据的时间消耗。
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public class GuavaCacheService {
public static void main(String[] args) {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.recordStats() //开启统计信息开关
.build();
cache.put("1", "v1");
cache.put("2", "v2");
cache.put("3", "v3");
cache.put("4", "v4");
cache.getIfPresent("1");
cache.getIfPresent("2");
cache.getIfPresent("3");
cache.getIfPresent("4");
cache.getIfPresent("5");
cache.getIfPresent("6");
System.out.println(cache.stats()); //获取统计信息
}
}
输出:CacheStats{hitCount=3, missCount=3, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=1}