①. 在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到Redis或Memcached 这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力
②. 随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构
③. 在先不考虑并发等复杂问题的情况下,两级缓存的访问流程可以用下面这张图来表示:

@Service
public class PositionServiceImpl implements PositionService {
@Autowired
PositionMapper positionMapper;
@Autowired
RedisTemplate redisTemplate;
private static Cache<Object, Object> cache = CacheBuilder.newBuilder().expireAfterWrite(5,TimeUnit.SECONDS).build();
@Override
public List<Position> getHotPosition() throws ExecutionException {
//从guava本地缓存中获取数据,如果没有则从redis中回源
Object value = cache.get("position", new Callable() {
@Override
public Object call() throws Exception {
return getHotPositionListFromRedis();
}
});
if(value != null){
return (List<Position>)value;
}
return null;
}
@Override
public List<Position> getHotPositionListFromRedis() {
Object position = redisTemplate.opsForValue().get("position");
System.out.println("从redis中获取数据");
if(position == null) {
//从mysql中获取数据
List<Position> positionList = positionMapper.select(null);
System.out.println("从mysql中获取数据");
//同步至redis
redisTemplate.opsForValue().set("position",positionList);
System.out.println("同步至redis");
redisTemplate.expire("position",5, TimeUnit.SECONDS);
//getHotPositionListFromRedis在guava不存在的时候调用,这里会将数据写入到guava本地缓存中
return positionList;
}
return (List<Position>)position;
}
}
②. Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)
Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。
③. Guava Cache的优势:
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>25.0-jreversion>
dependency>
private static void method1() throws ExecutionException, InterruptedException {
LoadingCache<String, String> cacheLoadInit = CacheBuilder
.newBuilder()
.initialCapacity(5)
.maximumSize(100)
.expireAfterWrite(5000, TimeUnit.MILLISECONDS) //
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> removalNotification) {
System.out.println(removalNotification.getKey() + ":remove==> :" + removalNotification.getValue());
}
})
.build(
new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println("first init.....");
return key;
}
});
//由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常
String key = cacheLoadInit.get("first-name");
String key2 = cacheLoadInit.get("first-name");
System.out.println(key);
TimeUnit.MILLISECONDS.sleep(5000);
System.out.println("==========");
//设置了5s的过期时间,5s后会重新加载缓存
String keySecond = cacheLoadInit.get("first-name");
System.out.println("5s后重新加载缓存数据" + keySecond);
/**
* first init.....
* first-name
* ==========
* first init.....
* 5s后重新加载缓存数据first-name
*/
}
private static void method2(){
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.initialCapacity(1)
.maximumSize(2)
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
System.out.println(notification.getKey()+"移除了,value:"+notification.getValue());
}
})
.build();
// getIfPresent(key):从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
String key1 = cache.getIfPresent("java金融1");
System.out.println(key1);//null
if(StringUtils.isEmpty(key1)){
/**
* null
* value - java金融2
* value - java金融3
*/
cache.put("java金融1","value - java金融1");
cache.put("java金融2","value - java金融2");
cache.put("java金融3","value - java金融3");
System.out.println(cache.getIfPresent("java金融1"));
System.out.println(cache.getIfPresent("java金融2"));
System.out.println(cache.getIfPresent("java金融3"));
}
}
/**
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
Cache cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
* @throws ExecutionException
*/
private static void method3() throws ExecutionException {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.initialCapacity(1)
.expireAfterWrite(1,TimeUnit.MINUTES)
.build();
cache.get("key1",new Callable<String>() {
@Override
public String call() throws Exception {
return "value1";
}
});
/**
* value1
* null
*/
System.out.println(cache.getIfPresent("key1"));
System.out.println("手动清除缓存");
cache.invalidateAll();
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
}
private static void maximumSizeMethod() throws ExecutionException {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.initialCapacity(1)
.maximumSize(3)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return key + " - value";
}
});
cache.get("1");
cache.get("2");
cache.get("3");
cache.get("4");
// 最大maximumSize=3 这里添加了四个元素进来了
// 这里是采用的URL 和 FIFO的方式去进行移除元素
/**
* null
* 2 - value
* 3 - value
* 4 - value
*/
System.out.println(cache.getIfPresent("1"));
System.out.println(cache.getIfPresent("2"));
System.out.println(cache.getIfPresent("3"));
System.out.println(cache.getIfPresent("4"));
}
private static void expireAfterAccessMethod() throws InterruptedException {
// 隔多长时间后没有被访问过的key被删除
Cache<String, String> build = CacheBuilder.newBuilder()
//缓存中的数据 如果3秒内没有访问则删除
.expireAfterAccess(3000, TimeUnit.MILLISECONDS)
.build();
build.put("1","1 - value");
build.put("2","2 - value");
build.put("3","3 - value");
Thread.sleep(1000);
build.getIfPresent("1");
Thread.sleep(2100);
// 这里在停顿了1s后,重新获取了,这个时候再次停顿2s,2,3没有发访问,过期了
System.out.println(build.getIfPresent("1"));
// 停顿3s后,这里也过期了
Thread.sleep(3000);
System.out.println("这里已经过了3s了");
System.out.println(build.getIfPresent("1"));
}
private static void expireAfterWriteDemo() throws ExecutionException, InterruptedException {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
// 等同于expire ttl 缓存中对象的生命周期就是3秒
.expireAfterWrite(3, TimeUnit.SECONDS)
.initialCapacity(1)
.maximumSize(5)
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> removalNotification) {
System.out.println("onRemoval ==> key:" + removalNotification.getKey() + "value:" + removalNotification.getValue());
}
})
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return s + " - value";
}
});
cache.get("1");
cache.get("2");
Thread.sleep(1000);
cache.getIfPresent("1");
Thread.sleep(2100);
// 虽然前面有访问,expireAfterWrite相当于ttl,这里返回的数据是null
System.out.println(cache.getIfPresent("1"));
}
public class GuavaExpireDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.weakValues()
.build();
Person value = new Person(1,"tang");
cache.put("1",value);
// Person(id=1, name=tang)
System.out.println(cache.getIfPresent("1"));
value=new Person(2,"yang");
// 强制垃圾回收
System.gc();
// null
System.out.println(cache.getIfPresent("1"));
}
}
①. 通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作
②. 缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值
public class removalListenerDemo {
public static void main(String[] args) {
Cache cache = CacheBuilder.newBuilder()
.initialCapacity(1)
.maximumSize(3)
.concurrencyLevel(1)
.removalListener(new RemovalListener() {
@Override
public void onRemoval(RemovalNotification removalNotification) {
System.out.println("removalListener => key:" + removalNotification.getKey() + ",value:" + removalNotification.getValue());
}
})
.build();
cache.put("key - 1","value - 1");
System.out.println(cache.getIfPresent("key - 1"));
cache.invalidate("key - 1");
System.out.println(cache.getIfPresent("key - 1"));
/**
value - 1
removalListener => key:key - 1,value:value - 1
null
*/
}
}
public class recordStatsDemo {
public static void main(String[] args) throws ExecutionException {
// 创建1块缓存,key和value都是integer类型,最大缓存个数是5,开启缓存统计功能
// 使用LoadingCache,如果数据不存在就使用CacheLoader加载数据
LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().recordStats().maximumSize(5).
build(new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer id) throws Exception {
System.out.println("mock query db....");
if (id % 2 == 0) {
Thread.sleep(100);
throw new RuntimeException();
} else {
Thread.sleep(200);
return id * 10;
}
}
});
// 预先添加一条缓存数据
cache.put(1, 100);
//....CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
System.out.println("...." + cache.stats());
// 缓存命中,hitCount加1
System.out.println("get data====" + cache.get(1));
//....CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
System.out.println("...." + cache.stats());
// 没有命中缓存, missCount和loadSuccessCount加1,并增加totalLoadTime(纳秒为单位)
System.out.println("get data====" + cache.get(3));
//....CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=214873400, evictionCount=0}
System.out.println("...." + cache.stats());
// 没有命中缓存, missCount和loadExceptionCount加1,并增加totalLoadTime(纳秒为单位)
try {
System.out.println("get data====" + cache.get(4));
} catch (Exception e) {
// ....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=318706100, evictionCount=0}
System.out.println("...." + cache.stats());
}
// 手动清除缓存数据,或者是直接操作缓存底层数据,不会影响统计信息
System.out.println("get data====" + cache.asMap().get(1));// 通过缓存底层数据结构,读取数据
cache.invalidateAll();// 清空缓存
//....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=317725800, evictionCount=0}
System.out.println("...." + cache.stats());
// 添加7条缓存数据,由于最大数目是5,所以evictionCount=2
System.out.println("size===" + cache.size());
cache.put(1, 100);
cache.put(2, 100);
cache.put(3, 100);
cache.put(4, 100);
cache.put(5, 100);
cache.put(6, 100);
cache.put(7, 100);
//....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=317725800, evictionCount=1}
System.out.println("...." + cache.stats());
}
}
private static void recordStatusMethod2() {
LoadingCache cache = CacheBuilder.newBuilder().recordStats().maximumSize(5).
build(new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer id) throws Exception {
System.out.println("mock query db....");
if (id % 2 == 0) {
Thread.sleep(100);
throw new RuntimeException();
} else {
Thread.sleep(200);
return id * 10;
}
}
});
// 修改底层的map也会导致cache发生变化
ConcurrentMap underlingMap = cache.asMap();
underlingMap.put(1, 1);
underlingMap.put(2, 2);
underlingMap.put(3, 3);
underlingMap.put(4, 4);
underlingMap.put(5, 5);
underlingMap.put(6, 6);
underlingMap.put(7, 6);
// underlingMap....{3=3, 6=6, 5=5, 7=6, 4=4} cache....5
System.out.println("underlingMap...." + underlingMap+"cache...." + cache.size());
// stats....CacheStats {hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=2}
System.out.println("stats...." + cache.stats());
}
public class GuavaAsMapDemo {
public static void main(String[] args) throws ExecutionException {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return "value:" + key;
}
});
cache.get("1");
cache.get("2");
cache.get("3");
ConcurrentMap<String, String> map = cache.asMap();
Set<String> strings = map.keySet();
System.out.println("获取到cache所有的key:"+strings);
Collection<String> values = map.values();
System.out.println("获取到cache所有的value:"+values);
Set<Map.Entry<String, String>> set = map.entrySet();
for (Map.Entry<String, String> stringStringEntry : set) {
System.out.println(stringStringEntry.getKey()+"<====>"+stringStringEntry.getValue());
}
map.get("4");
System.out.println("从缓存中获取key4:"+cache.getIfPresent("4"));
/**
* 获取到cache所有的key:[1, 3, 2]
* 获取到cache所有的value:[value:1, value:3, value:2]
* 1<====>value:1
* 3<====>value:3
* 2<====>value:2
*/
}
}
/**
refreshAfterWrites — 失效后异步刷新缓存
使用refreshAfterWrites后,需要实现CacheLoader的reload方法。需要在方法中创建一个ListenableFutureTask,然后将这个task提交给线程池去异步执行。这样的话,缓存失效后重新加载就变成了异步,加载期间尝试获取取缓存的线程也不会被阻塞。而是获取到加载之前的值。加载完毕之后,各个线程就能取到最新的值。
总结:refreshAfterWrites是异步去刷新缓存的方法,使用过期的旧值快速响应。而expireAfterWrites缓存失效后线程需要同步等待加载结果,可能会造成请求大量堆积的问题。
**/
@SuppressWarnings("all")
public class TestGuavaCache {
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
static LoadingCache<String, String> cache = CacheBuilder.
newBuilder().
refreshAfterWrite(2, TimeUnit.SECONDS).
build(new CacheLoader<String, String>() {
//同步加载缓存
@Override
public String load(String key) throws Exception {
System.out.println("============");
return "";
}
//异步加载缓存
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
//定义任务。
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("================");
return "曹操";
});
//异步执行任务
executorService.execute(futureTask);
return futureTask;
}
});
public static void main(String[] args) throws ExecutionException, InterruptedException {
cache.put("name", "李白");
//第一次获取缓存:李白
System.out.println(cache.get("name"));
//睡眠2s后,再次获取,此时缓存失效,异步的加载缓存。但是线程是立即返回“旧结果”。
Thread.sleep(2000);
System.out.println(cache.get("name"));
Thread.sleep(1000);
System.out.println(cache.get("name"));
}
}
CacheBuilder.newBuilder()
.refreshAfterWrite(20, TimeUnit.MINUTES)
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(1)
.build(new CacheLoader<String, List<Map<String, Long>>>() {
@Override
public List<Map<String, Long>> load(String s) throws Exception {
return queryData();
}
@Override
public ListenableFuture<List<Map<String, Long>>> reload(String key, List<Map<String, Long>> oldValue)
throws Exception {
ListenableFutureTask<List<Map<String, Long>>> task = ListenableFutureTask
.create(() -> queryData());
executorService.execute(task);
return task;
}
});
①. Guava Cache的数据结构跟ConcurrentHashMap类似,但也不完全一样。最基本的区别是
ConcurrentMap会一直保存所有添加的元素,直到显式地移除
②. 相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。其数据结构图如下:

③. LocalCache为Guava Cache的核心类,包含一个Segment数组组成。