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}