目前大部分互联网架构 Cache 已经成为了必可不少的一环。常用的方案有大家熟知的 NoSQL 数据库(Redis、Memcached),也有大量的进程内缓存比如 EhCache 、Guava Cache、Caffeine 等。
本系列文章会选取本地缓存和分布式缓存(NoSQL)的优秀框架比较他们各自的优缺点、应用场景、项目中的最佳实践以及原理分析。本文主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析。
基本用法Guava Cache 通过简单好用的 Client 可以快速构造出符合需求的 Cache 对象,不需要过多复杂的配置,大多数情况就像构造一个 POJO 一样的简单。这里介绍两种构造 Cache 对象的方式:CacheLoader 和 Callable
CacheLoader构造 LoadingCache 的关键在于实现 load 方法,也就是在需要 访问的缓存项不存在的时候 Cache 会自动调用 load 方法将数据加载到 Cache 中。这里你肯定会想假如有多个线程过来访问这个不存在的缓存项怎么办,也就是缓存的并发问题如何怎么处理是否需要人工介入,这些在下文中也会介绍到。
除了实现 load 方法之外还可以配置缓存相关的一些性质,比如过期加载策略、刷新策略 。
private static final LoadingCache<String, String> CACHE = CacheBuilder .newBuilder() // 最大容量为 100 超过容量有对应的淘汰机制,下文详述 .maximumSize(100) // 缓存项写入后多久过期,下文详述 .expireAfterWrite(60 * 5, TimeUnit.SECONDS) // 缓存写入后多久自动刷新一次,下文详述 .refreshAfterWrite(60, TimeUnit.SECONDS) // 创建一个 CacheLoader,load 表示缓存不存在的时候加载到缓存并返回 .build(new CacheLoader<String, String>() { // 加载缓存数据的方法 @Override public String load(String key) { return "cache [" + key + "]"; } }); public void getTest() throws Exception { CACHE.get("KEY_25487"); }Callable
除了在构造 Cache 对象的时候指定 load 方法来加载缓存外,我们亦可以在获取缓存项时指定载入缓存的方法,并且可以根据使用场景在不同的位置采用不同的加载方式。
比如在某些位置可以通过二级缓存加载不存在的缓存项,而有些位置则可以直接从 DB 加载缓存项。
// 注意返回值是 Cache private static final Cache<String, String> SIMPLE_CACHE = CacheBuilder .newBuilder() .build(); public void getTest1() throws Exception { String key = "KEY_25487"; // get 缓存项的时候指定 callable 加载缓存项 SIMPLE_CACHE.get(key, () -> "cache [" + key + "]"); } 缓存项加载机制如果某个缓存过期了或者缓存项不存在于缓存中,而恰巧此此时有大量请求过来请求这个缓存项,如果没有保护机制就会导致大量的线程同时请求数据源加载数据并生成缓存项,这就是所谓的 “缓存击穿” 。
举个简单的例子,某个时刻有 100 个请求同时请求 KEY_25487 这个缓存项,而不巧这个缓存项刚好失效了,那么这 100 个线程(如果有这么多机器和流量的话)就会同时从 DB 加载这个数据,很可怕的点在于就算某一个线程率先获取到数据生成了缓存项,其他的线程还是继续请求 DB 而不会走到缓存。
【缓存击穿图例】