前言
大家在做项目时,遇到的第一个问题就是如何提高服务器的性能,从而提升用户的体验。当遇到这个问题时,不可避免的就会引出了 缓存
这个概念。
而 缓存
(主要从服务端介绍) 又分 本地缓存 和 分布式缓存 以及 数据库缓存。这三种缓存分别适应不同的场景,这里我们先介绍一下本地缓存。
说到本地缓存,就不得不说到 caffeine
,caffeine 被称之为缓存之王。它为什么被这么多人推崇呢?这离不开它的高性能。
Caffeine 简介
首先,Caffeine
是基于 Java 8 的高性能,接近最佳的缓存库。
并且它 使用 Google Guava
启发的 API 提供内存缓存。 改进取决于您设计 Guava
缓存和 ConcurrentLinkedHashMap
的体验。
核心实现代码举例:
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
Caffeine
的底层数据存储采用ConcurrentHashMap
。因为Caffeine
面向JDK8,在jdk8中ConcurrentHashMap
增加了红黑树,在hash冲突严重时也能有良好的读性能。
caffeine 入门使用
引入 caffine
1、在 pom.xml
中添加 caffeine
依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
2、创建一个 Caffeine
缓存(类似一个map):
Cache<String, Object> manualCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
这里介绍一下常见用法:
public static void main(String... args) throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
//5秒没有读写自动删除
.expireAfterAccess(5, TimeUnit.SECONDS)
//最大容量1024个,超过会自动清理空间
.maximumSize(1024)
//移除监听
.removalListener(((key, value, cause) -> {
//清理通知 key,value ==> 键值对 cause ==> 清理原因
}))
.build();
}
调用:
//添加值
cache.put("张三", "浙江");
//获取值
cache.getIfPresent("张三");
//remove
cache.invalidate("张三");
有时候我们会需要它自动加载,也就是说需要自动把数据加载到缓存内,而不是手动加载。
有一下几个方法:
填充策略
填充策略是指如何在key不存在的情况下,如何创建一个对象进行返回,主要分为下面四种:
1、手动(Manual)(不推荐)
public static void main(String... args) throws Exception {
Cache<String, Integer> cache = Caffeine.newBuilder().build();
Integer age1 = cache.getIfPresent("张三");
System.out.println(age1);
//当key不存在时,会立即创建出对象来返回,age2不会为空
Integer age2 = cache.get("张三", k -> {
System.out.println("k:" + k);
return 18;
});
System.out.println(age2);
}
2、自动(Loading)(推荐)
public static void main(String... args) throws Exception {
//此时的类型是 LoadingCache 不是 Cache
LoadingCache<String, Integer> cache = Caffeine.newBuilder().build(key -> {
System.out.println("自动填充:" + key);
return 18;
});
Integer age1 = cache.getIfPresent("张三");
System.out.println(age1);
// key 不存在时 会根据给定的CacheLoader自动装载进去
Integer age2 = cache.get("张三");
System.out.println(age2);
}
3、异步手动(Asynchronous Manual)
public static void main(String... args) throws Exception {
AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();
//会返回一个 future对象, 调用future对象的get方法会一直卡住直到得到返回,和多线程的submit一样
CompletableFuture<Integer> ageFuture = cache.get("张三", name -> {
System.out.println("name:" + name);
return 18;
});
Integer age = ageFuture.get();
System.out.println("age:" + age);
}
4、异步自动(Asynchronously Loading)
public static void main(String... args) throws Exception {
//和1.4基本差不多
AsyncLoadingCache<String, Integer> cache = Caffeine.newBuilder().buildAsync(name -> {
System.out.println("name:" + name);
return 18;
});
CompletableFuture<Integer> ageFuture = cache.get("张三");
Integer age = ageFuture.get();
System.out.println("age:" + age);
}
源码示例
@RestController
public class CaffeineCacheController {
@Autowired
PersonService personService;
Cache<String, Object> manualCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// Either: Build with a synchronous computation that is wrapped as asynchronous
.buildAsync(key -> createExpensiveGraph(key));
// Or: Build with a asynchronous computation that returns a future
// .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
private CompletableFuture<Object> createExpensiveGraphAsync(String key, Executor executor) {
CompletableFuture<Object> objectCompletableFuture = new CompletableFuture<>();
return objectCompletableFuture;
}
private Object createExpensiveGraph(String key) {
System.out.println("缓存不存在或过期,调用了createExpensiveGraph方法获取缓存key的值");
if (key.equals("name")) {
throw new RuntimeException("调用了该方法获取缓存key的值的时候出现异常");
}
return personService.findOne1();
}
@RequestMapping("/testManual")
public Object testManual(Person person) {
String key = "name1";
Object graph = null;
// 根据key查询一个缓存,如果没有返回NULL
graph = manualCache.getIfPresent(key);
// 根据Key查询一个缓存,如果没有调用createExpensiveGraph方法,并将返回值保存到缓存。
// 如果该方法返回Null则manualCache.get返回null,如果该方法抛出异常则manualCache.get抛出异常
graph = manualCache.get(key, k -> createExpensiveGraph(k));
// 将一个值放入缓存,如果以前有值就覆盖以前的值
manualCache.put(key, graph);
// 删除一个缓存
manualCache.invalidate(key);
ConcurrentMap<String, Object> map = manualCache.asMap();
System.out.println(map.toString());
return graph;
}
@RequestMapping("/testLoading")
public Object testLoading(Person person) {
String key = "name1";
// 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个createExpensiveGraph函数。
// 查询并在缺失的情况下使用同步的方式来构建一个缓存
Object graph = loadingCache.get(key);
// 获取组key的值返回一个Map
List<String> keys = new ArrayList<>();
keys.add(key);
Map<String, Object> graphs = loadingCache.getAll(keys);
return graph;
}
@RequestMapping("/testAsyncLoading")
public Object testAsyncLoading(Person person) {
String key = "name1";
// 查询并在缺失的情况下使用异步的方式来构建缓存
CompletableFuture<Object> graph = asyncLoadingCache.get(key);
// 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存
List<String> keys = new ArrayList<>();
keys.add(key);
CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
// 异步转同步
loadingCache = asyncLoadingCache.synchronous();
return graph;
}
@RequestMapping("/testSizeBased")
public Object testSizeBased(Person person) {
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(1)
.build(k -> createExpensiveGraph(k));
cache.get("A");
System.out.println(cache.estimatedSize());
cache.get("B");
// 因为执行回收的方法是异步的,所以需要调用该方法,手动触发一次回收操作。
cache.cleanUp();
System.out.println(cache.estimatedSize());
return "";
}
@RequestMapping("/testTimeBased")
public Object testTimeBased(Person person) {
String key = "name1";
// 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
FakeTicker ticker = new FakeTicker();
// 基于固定的到期策略进行退出
// expireAfterAccess
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.ticker(ticker::read)
.expireAfterAccess(5, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
System.out.println("expireAfterAccess:第一次获取缓存");
cache1.get(key);
System.out.println("expireAfterAccess:等待4.9S后,第二次次获取缓存");
// 直接指定时钟
ticker.advance(4900, TimeUnit.MILLISECONDS);
cache1.get(key);
System.out.println("expireAfterAccess:等待0.101S后,第三次次获取缓存");
ticker.advance(101, TimeUnit.MILLISECONDS);
cache1.get(key);
// expireAfterWrite
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
.ticker(ticker::read)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
System.out.println("expireAfterWrite:第一次获取缓存");
cache2.get(key);
System.out.println("expireAfterWrite:等待4.9S后,第二次次获取缓存");
ticker.advance(4900, TimeUnit.MILLISECONDS);
cache2.get(key);
System.out.println("expireAfterWrite:等待0.101S后,第三次次获取缓存");
ticker.advance(101, TimeUnit.MILLISECONDS);
cache2.get(key);
// Evict based on a varying expiration policy
// 基于不同的到期策略进行退出
LoadingCache<String, Object> cache3 = Caffeine.newBuilder()
.ticker(ticker::read)
.expireAfter(new Expiry<String, Object>() {
@Override
public long expireAfterCreate(String key, Object value, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
return TimeUnit.SECONDS.toNanos(5);
}
@Override
public long expireAfterUpdate(String key, Object graph,
long currentTime, long currentDuration) {
System.out.println("调用了 expireAfterUpdate:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
return currentDuration;
}
@Override
public long expireAfterRead(String key, Object graph,
long currentTime, long currentDuration) {
System.out.println("调用了 expireAfterRead:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
return currentDuration;
}
})
.build(k -> createExpensiveGraph(k));
System.out.println("expireAfter:第一次获取缓存");
cache3.get(key);
System.out.println("expireAfter:等待4.9S后,第二次次获取缓存");
ticker.advance(4900, TimeUnit.MILLISECONDS);
cache3.get(key);
System.out.println("expireAfter:等待0.101S后,第三次次获取缓存");
ticker.advance(101, TimeUnit.MILLISECONDS);
Object object = cache3.get(key);
return object;
}
@RequestMapping("/testRemoval")
public Object testRemoval(Person person) {
String key = "name1";
// 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
FakeTicker ticker = new FakeTicker();
// 基于固定的到期策略进行退出
// expireAfterAccess
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.removalListener((String k, Object graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", k, cause))
.ticker(ticker::read)
.expireAfterAccess(5, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
System.out.println("第一次获取缓存");
Object object = cache.get(key);
System.out.println("等待6S后,第二次次获取缓存");
// 直接指定时钟
ticker.advance(6000, TimeUnit.MILLISECONDS);
cache.get(key);
System.out.println("手动删除缓存");
cache.invalidate(key);
return object;
}
@RequestMapping("/testRefresh")
public Object testRefresh(Person person) {
String key = "name1";
// 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
FakeTicker ticker = new FakeTicker();
// 基于固定的到期策略进行退出
// expireAfterAccess
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.removalListener((String k, Object graph, RemovalCause cause) ->
System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
.ticker(ticker::read)
.expireAfterWrite(5, TimeUnit.SECONDS)
// 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
.refreshAfterWrite(4, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
System.out.println("第一次获取缓存");
Object object = cache.get(key);
System.out.println("等待4.1S后,第二次次获取缓存");
// 直接指定时钟
ticker.advance(4100, TimeUnit.MILLISECONDS);
cache.get(key);
System.out.println("等待5.1S后,第三次次获取缓存");
// 直接指定时钟
ticker.advance(5100, TimeUnit.MILLISECONDS);
cache.get(key);
return object;
}
@RequestMapping("/testWriter")
public Object testWriter(Person person) {
String key = "name1";
// 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
FakeTicker ticker = new FakeTicker();
// 基于固定的到期策略进行退出
// expireAfterAccess
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.removalListener((String k, Object graph, RemovalCause cause) ->
System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
.ticker(ticker::read)
.expireAfterWrite(5, TimeUnit.SECONDS)
.writer(new CacheWriter<String, Object>() {
@Override
public void write(String key, Object graph) {
// write to storage or secondary cache
// 写入存储或者二级缓存
System.out.printf("testWriter:write - Key %s was write (%s)%n", key, graph);
createExpensiveGraph(key);
}
@Override
public void delete(String key, Object graph, RemovalCause cause) {
// delete from storage or secondary cache
// 删除存储或者二级缓存
System.out.printf("testWriter:delete - Key %s was delete (%s)%n", key, graph);
}
})
// 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
.refreshAfterWrite(4, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
cache.put(key, personService.findOne1());
cache.invalidate(key);
System.out.println("第一次获取缓存");
Object object = cache.get(key);
System.out.println("等待4.1S后,第二次次获取缓存");
// 直接指定时钟
ticker.advance(4100, TimeUnit.MILLISECONDS);
cache.get(key);
System.out.println("等待5.1S后,第三次次获取缓存");
// 直接指定时钟
ticker.advance(5100, TimeUnit.MILLISECONDS);
cache.get(key);
return object;
}
@RequestMapping("/testStatistics")
public Object testStatistics(Person person) {
String key = "name1";
// 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
FakeTicker ticker = new FakeTicker();
// 基于固定的到期策略进行退出
// expireAfterAccess
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.removalListener((String k, Object graph, RemovalCause cause) ->
System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
.ticker(ticker::read)
.expireAfterWrite(5, TimeUnit.SECONDS)
// 开启统计
.recordStats()
// 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
.refreshAfterWrite(4, TimeUnit.SECONDS)
.build(k -> createExpensiveGraph(k));
for (int i = 0; i < 10; i++) {
cache.get(key);
cache.get(key + i);
}
// 驱逐是异步操作,所以这里要手动触发一次回收操作
ticker.advance(5100, TimeUnit.MILLISECONDS);
// 手动触发一次回收操作
cache.cleanUp();
System.out.println("缓存命数量:" + cache.stats().hitCount());
System.out.println("缓存命中率:" + cache.stats().hitRate());
System.out.println("缓存逐出的数量:" + cache.stats().evictionCount());
System.out.println("加载新值所花费的平均时间:" + cache.stats().averageLoadPenalty());
return cache.get(key);
}
@RequestMapping("/testPolicy")
public Object testPolicy(Person person) {
FakeTicker ticker = new FakeTicker();
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.ticker(ticker::read)
.expireAfterAccess(5, TimeUnit.SECONDS)
.maximumSize(1)
.build(k -> createExpensiveGraph(k));
// 在代码里面动态的指定最大Size
cache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(4 * eviction.getMaximum());
});
cache.get("E");
cache.get("B");
cache.get("C");
cache.cleanUp();
System.out.println(cache.estimatedSize() + ":" + JSON.toJSON(cache.asMap()).toString());
cache.get("A");
ticker.advance(100, TimeUnit.MILLISECONDS);
cache.get("D");
ticker.advance(100, TimeUnit.MILLISECONDS);
cache.get("A");
ticker.advance(100, TimeUnit.MILLISECONDS);
cache.get("B");
ticker.advance(100, TimeUnit.MILLISECONDS);
cache.policy().eviction().ifPresent(eviction -> {
// 获取热点数据Map
Map<String, Object> hottestMap = eviction.hottest(10);
// 获取冷数据Map
Map<String, Object> coldestMap = eviction.coldest(10);
System.out.println("热点数据:" + JSON.toJSON(hottestMap).toString());
System.out.println("冷数据:" + JSON.toJSON(coldestMap).toString());
});
ticker.advance(3000, TimeUnit.MILLISECONDS);
// ageOf通过这个方法来查看key的空闲时间
cache.policy().expireAfterAccess().ifPresent(expiration -> {
System.out.println(JSON.toJSON(expiration.ageOf("A", TimeUnit.MILLISECONDS)));
});
return cache.get("name1");
}
}