概述
1 快速入门案例
maven依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.4</version>
</dependency>
1.1 构建cache工具类
public enum LoadTypeEnum {
/**
* 手动加载
*/
MANUAL,
/**
* 同步加载
*/
SYNC,
/**
* 异步加载
*/
ASYNC,
/**
* 后台刷新
*/
REFRESH
}
@Slf4j
public class CacheUtil {
private static final Random RANDOM = new Random();
private static ExecutorService executorService = Executors.newCachedThreadPool();
public static int randomInt() {
return RANDOM.nextInt(1000);
}
public static void execute(Runnable runnable) {
executorService.execute(runnable);
}
public static LoadingCache buildLoadCache(CacheConfig config) {
return (LoadingCache) buildLoadCache(LoadTypeEnum.REFRESH, config.getInitialCapacity(), config.getMaximumSize());
}
public static Object buildLoadCache(LoadTypeEnum loadTypeEnum) {
return buildLoadCache(loadTypeEnum, 10, 100);
}
private static Object buildLoadCache(LoadTypeEnum loadTypeEnum, int initialCapacity, int maximumSize) {
Caffeine<String, Object> caffeine = Caffeine.newBuilder()
// 初始容量
.initialCapacity(initialCapacity)
// 最大容量为(基于容量进行回收)
.maximumSize(maximumSize)
// 监控
.recordStats()
//当Entry被移除时的监听器
.removalListener((key, value, removalCause) -> log.info("remove cache:{}-{}, cause:{}", key, value, removalCause));
if (loadTypeEnum == LoadTypeEnum.REFRESH) {
// 配置写入后多久调用load方法刷新缓存
caffeine.refreshAfterWrite(5, TimeUnit.SECONDS);
} else {
// 配置写入后多久使缓存过期
caffeine.expireAfterWrite(5, TimeUnit.SECONDS);
}
if (loadTypeEnum == LoadTypeEnum.ASYNC) {
// 异步加载
return caffeine.buildAsync(key -> {
String value = "value_" + CacheUtil.randomInt();
log.info(Thread.currentThread().getName() + " load value:{}, cost 3s", value);
Thread.sleep(3000);
return value;
});
} else if (loadTypeEnum != LoadTypeEnum.MANUAL) {
// 同步加载
return caffeine.build(key -> {
String value = "value_" + CacheUtil.randomInt();
log.info(Thread.currentThread().getName() + " load value:{}, cost 3s", value);
Thread.sleep(3000);
return value;
});
} else {
// 手动加载
return caffeine.build();
}
}
}
1.2 手动加载测试
@Slf4j
public class ManualCacheTest {
private Cache<String, Object> cache = (Cache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.MANUAL);
@Test
public void test() throws InterruptedException {
String key = "key1";
cache.put(key, "v1");
log.info("exist key:" + key + "--->" + cache.getIfPresent(key));
log.info("not exist key:key2" + "--->" + cache.getIfPresent("key2"));
// 覆盖key
cache.put(key, "v2");
log.info("override key:" + key + "--->" + cache.getIfPresent(key));
// 作废key
cache.invalidate(key);
log.info("invalid key:" + key + "--->" + cache.getIfPresent(key));
// 过期失效
cache.put(key, "v1");
log.info("exist key:" + key + "--->" + cache.getIfPresent(key));
Thread.sleep(6000);
log.info("expired key:" + key + "--->" + cache.getIfPresent(key));
}
}
输出
16:28:37.193 [main] INFO com.cache.demo.ManualCacheTest - exist key:key1--->v1
16:28:37.197 [main] INFO com.cache.demo.ManualCacheTest - not exist key:key2--->null
16:28:37.201 [main] INFO com.cache.demo.ManualCacheTest - override key:key1--->v2
16:28:37.201 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:key1-v1, cause:REPLACED
16:28:37.203 [main] INFO com.cache.demo.ManualCacheTest - invalid key:key1--->null
16:28:37.203 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:key1-v2, cause:EXPLICIT
16:28:37.204 [main] INFO com.cache.demo.ManualCacheTest - exist key:key1--->v1
16:28:43.206 [main] INFO com.cache.demo.ManualCacheTest - expired key:key1--->null
1.3 同步加载测试
@Slf4j
public class SyncLoadTest {
LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.SYNC);
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 2; i++) {
CacheUtil.execute(() -> {
while (true) {
try {
log.info(Thread.currentThread().getName() + " " + loadingCache.get("123456"));
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
TimeUnit.HOURS.sleep(2);
}
}
输出
16:31:04.328 [pool-1-thread-1] INFO com.cache.CacheUtil - pool-1-thread-1 load value:value_647, cost 3s
16:31:07.342 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:07.344 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:08.344 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:08.344 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:09.347 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:09.347 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:10.348 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:10.348 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:11.353 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:11.353 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:12.359 [pool-1-thread-1] INFO com.cache.CacheUtil - pool-1-thread-1 load value:value_822, cost 3s
16:31:15.361 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_822
16:31:15.363 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:123456-value_647, cause:EXPIRED
16:31:15.363 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_822
可以看到缓存过期后其中一个读线程阻塞去加载数据,别的线程都阻塞等待
1.4 异步加载测试
@Slf4j
public class AsyncCacheTest {
AsyncLoadingCache<String, Object> loadingCache = (AsyncLoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.ASYNC);
@Test
public void test() {
while (true) {
try {
CompletableFuture<Object> future = loadingCache.get("123456");
Thread.sleep(1000);
log.info("执行别的业务,耗时1s");
log.info("读取缓存:" + future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
输出
16:38:22.751 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:22.751 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_922
16:38:23.755 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:23.756 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_922
16:38:23.758 [ForkJoinPool.commonPool-worker-2] INFO com.cache.CacheUtil - ForkJoinPool.commonPool-worker-2 load value:value_457, cost 3s
16:38:23.759 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:123456-value_922, cause:EXPIRED
16:38:24.763 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:26.763 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_457
16:38:27.764 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
可以看到异步加载利用future模式实现加载缓存的同时不影响别的业务
1.5 自动刷新缓存
public class RefreshLoadTest {
LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.REFRESH);
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 2; i++) {
CacheUtil.execute(() -> {
while (true) {
try {
log.info(Thread.currentThread().getName() + " " + loadingCache.get("123456"));
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
TimeUnit.HOURS.sleep(2);
}
}
输出
16:41:20.254 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:21.254 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:21.255 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:22.258 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:22.261 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - ForkJoinPool.commonPool-worker-1 load value:value_138, cost 3s
16:41:22.265 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:23.263 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:23.269 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:24.267 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:24.270 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:25.269 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:25.272 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:25.277 [ForkJoinPool.commonPool-worker-2] INFO com.cache.CacheUtil - remove cache:123456-value_704, cause:REPLACED
16:41:26.270 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_138
16:41:26.274 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_138
16:41:27.274 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_138
可以看到缓存过期后读线程都返回老数据,单开一个线程加载数据
1.6 统计命中率
public class CacheStatTest {
LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.REFRESH);
@Test
public void test() throws InterruptedException {
for (int j = 0; j < 5; j++) {
for (int i = 0; i < 10; i++) {
loadingCache.get(i + "");
}
}
loadingCache.get("11");
CacheStats stats = loadingCache.stats();
log.info("hit count:{}, missCount:{}, hit rate:{}",
stats.hitCount(), stats.missCount(), stats.hitRate());
TimeUnit.HOURS.sleep(1);
}
}
测试前先注释掉工具类中的睡眠代码
输出:16:51:45.128 [main] INFO com.cache.demo.CacheStatTest - hit count:40, missCount:11, hit rate:0.7843137254901961
2 源码浅析
开发中常用1.5 自动刷新缓存,就看看LoadingCache.get()中做了什么
追到com.github.benmanes.caffeine.cache.BoundedLocalCache#computeIfAbsent
2.1 computeIfAbsent方法概览
public @Nullable V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
boolean recordStats, boolean recordLoad) {
requireNonNull(key);
// mappingFunction就是用cacheLoader构建的
requireNonNull(mappingFunction);
long now = expirationTicker().read();
// data是ConcurrentHashMap
Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
if (node != null) {
V value = node.getValue();
if ((value != null) && !hasExpired(node, now)) {
if (!isComputingAsync(node)) {
// 如果采用expireAfterRead过期策略则修改过期时间
tryExpireAfterRead(node, key, value, expiry(), now);
// 修改缓存访问时间
setAccessTime(node, now);
}
// 读缓存后的操作
afterRead(node, now, /* recordHit */ recordStats);
return value;
}
}
if (recordStats) {
// 统计信息,没有命中次数增加
mappingFunction = statsAware(mappingFunction, recordLoad);
}
// 根据引用类型重新构建key
Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
// 2.2 计算缓存value
return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
}
computeIfAbsent方法主要做了如下操作
1 根据key取node
2 如果node不为空且没过期则做一些额外操作,然后返回value
3 如果node为空则先统计命中信息,然后加载缓存
2.2 doComputeIfAbsent方法分析
@Nullable V doComputeIfAbsent(K key, Object keyRef,
Function<? super K, ? extends V> mappingFunction, long[/* 1 */] now, boolean recordStats) {
// 旧值引用
V[] oldValue = (V[]) new Object[1];
// 新值引用
V[] newValue = (V[]) new Object[1];
// key引用
K[] nodeKey = (K[]) new Object[1];
// 移除的node引用
Node<K, V>[] removed = new Node[1];
int[] weight = new int[2]; // old, new
RemovalCause[] cause = new RemovalCause[1];
Node<K, V> node = data.compute(keyRef, (k, n) -> {
if (n == null) {
// 根据key获取新的value
newValue[0] = mappingFunction.apply(key);
if (newValue[0] == null) {
return null;
}
now[0] = expirationTicker().read();
weight[1] = weigher.weigh(key, newValue[0]);
// 构建value node
n = nodeFactory.newNode(key, keyReferenceQueue(),
newValue[0], valueReferenceQueue(), weight[1], now[0]);
setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
return n;
}
// 如果旧值不为空则进行如下处理
synchronized (n) {
// 根据老的k-v设置移除原因
nodeKey[0] = n.getKey();
weight[0] = n.getWeight();
oldValue[0] = n.getValue();
if ((nodeKey[0] == null) || (oldValue[0] == null)) {
cause[0] = RemovalCause.COLLECTED;
} else if (hasExpired(n, now[0])) {
cause[0] = RemovalCause.EXPIRED;
} else {
return n;
}
writer.delete(nodeKey[0], oldValue[0], cause[0]);
// 计算新值
newValue[0] = mappingFunction.apply(key);
if (newValue[0] == null) {
removed[0] = n;
// 旧值清理
n.retire();
return null;
}
weight[1] = weigher.weigh(key, newValue[0]);
// 旧node设置新值
n.setValue(newValue[0], valueReferenceQueue());
n.setWeight(weight[1]);
now[0] = expirationTicker().read();
// 设置读写时间
setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
setAccessTime(n, now[0]);
setWriteTime(n, now[0]);
return n;
}
});
if (node == null) {
if (removed[0] != null) {
// 添加一个旧node移除任务
afterWrite(new RemovalTask(removed[0]));
}
return null;
}
if (cause[0] != null) {
if (hasRemovalListener()) {
// 如果旧值移除原因不为空并且配置了移除监听则进行通知
notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
}
statsCounter().recordEviction(weight[0], cause[0]);
}
if (newValue[0] == null) {
if (!isComputingAsync(node)) {
// 如果设置了expireAfterRead则延长过期时间
tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
// 设置访问时间
setAccessTime(node, now[0]);
}
// node读取后的操作
afterRead(node, now[0], /* recordHit */ recordStats);
// 返回旧值
return oldValue[0];
}
if ((oldValue[0] == null) && (cause[0] == null)) {
// 添加一个添加node的任务
afterWrite(new AddTask(node, weight[1]));
} else {
int weightedDifference = (weight[1] - weight[0]);
// 添加一个修改node的任务
afterWrite(new UpdateTask(node, weightedDifference));
}
return newValue[0];
}
doComputeIfAbsent主要做了如下操作
1 计算新值
2 旧值移除相关操作(添加移除任务,旧值移除监听处理)
3 新值添加相关操作(添加添加node、修改node任务)
最后
以上就是文艺红酒为你收集整理的内存缓存-caffeine1 快速入门案例2 源码浅析的全部内容,希望文章能够帮你解决内存缓存-caffeine1 快速入门案例2 源码浅析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复