Guava LoadingCache本地缓存的正确使用姿势——异步加载
1. 【背景】AB实验SDK耗时过高
同事在使用我写的实验平台sdk之后,吐槽耗时太高,获取实验数据分流耗时达到700ms,严重影响了主业务流程的执行
2. 【分析】缓存为何不管用
我记得之前在sdk端加了本地缓存(使用了LoadingCache),不应该这样慢
通过分析,只有在缓存失效之后的那一次请求耗时会比较高,又因为随着实验数据的增加,获取实验确实会花费这么多时间
那如何解决呢?如果不解决,每次缓存失效,至少会有一个请求阻塞获取实验数据导致超时
3. 【工具】Guava LoadingCache
Guava是一个谷歌开源Java工具库,提供了一些非常实用的工具。LoadingCache就是其中一个,是一个本地缓存工具,支持配置加载函数,定时失效
基本用法:
- 其中的CacheLoader是当key对应value不存在时,会使用重载的load方法取并放入cache
- cache.get从缓存获取数据
LoadingCache<Long, String> cache
// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
// 设置并发级别为3,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(3)
// 过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始容量
.initialCapacity(1000)
// 最大容量,超过LRU
.maximumSize(2000).build(new CacheLoader<Long, String>() {
@Override
@Nonnull
public String load(@Nonnull Long key) throws Exception {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
}
});
// 获取数据
cache.get(1L)
3.1 LoadingCache的失效和刷新
既然用到缓存,避免不了的问题就是如何更新缓存中的值,使其不能太旧,又能兼顾性能
LoadingCache常用两个方法来实现失效:
expireAfterWrite(long, TimeUnit)refreshAfterWrite(long, TimeUnit)
官方文档给出的区别
Refreshing is not quite the same as eviction. As specified in LoadingCache.refresh(K), refreshing a key loads a new value for the key, possibly asynchronously. The old value (if any) is still returned while the key is being refreshed, in contrast to eviction, which forces retrievals to wait until the value is loaded anew
- refresh期间会返回旧值
- expire会等待load方法的新值
我们的场景就是某个请求会阻塞等待数据返回,所以如果我们用refresh方法过期的话,就能使耗时变低,带来的问题是当时获取的数据是旧的,对于当前这个场景是可以接受的
3.2 refreshAfterWrite如何异步加载
3.2.1 验证expireAfterWrite
public static void testExpireAfterWrite() throws ExecutionException, InterruptedException {
LoadingCache<Long, String> cache
// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
// 设置并发级别为3,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(3)
// 过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始容量
.initialCapacity(1000)
// 最大容量,超过LRU
.maximumSize(2000).build(new CacheLoader<Long, String>() {
@Override
@Nonnull
public String load(@Nonnull Long key) throws Exception {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
}
});
log.info("cache get");
String rs = cache.get(10L);
log.info("cache rs:{}", rs);
Thread.sleep(6000);
log.info("cache get");
rs = cache.get(10L);
log.info("cache rs:{}", rs);
}
输出结果,从打印的时间可以看出,第二次get同步等待结果
15:33:44.160 [main] INFO cache.LoadingCacheTest - cache get
15:33:45.192 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:33:45
15:33:51.199 [main] INFO cache.LoadingCacheTest - cache get
15:33:52.225 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:33:52
3.2.2 验证refreshAfterWrite
public static void testRefreshAfterWrite() throws ExecutionException, InterruptedException {
LoadingCache<Long, String> cache
// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
// 设置并发级别为3,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(3)
// 过期
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 初始容量
.initialCapacity(1000)
// 最大容量,超过LRU
.maximumSize(2000).build(new CacheLoader<Long, String>() {
@Override
@Nonnull
public String load(@Nonnull Long key) throws Exception {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
}
});
log.info("cache get");
String rs = cache.get(10L);
log.info("cache rs:{}", rs);
Thread.sleep(6000);
log.info("cache get");
rs = cache.get(10L);
log.info("cache rs:{}", rs);
}
输出结果,从打印的时间可以看出,第二次也get同步等待结果
15:35:31.064 [main] INFO cache.LoadingCacheTest - cache get
15:35:32.090 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:35:32
15:35:38.099 [main] INFO cache.LoadingCacheTest - cache get
15:35:39.147 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:35:39
3.2.3 验证refreshAfterWrite加线程池
public static void testRefreshAfterWriteWithReload() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
LoadingCache<Long, String> cache
// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
// 设置并发级别为3,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(3)
// 过期
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 初始容量
.initialCapacity(1000)
// 最大容量,超过LRU
.maximumSize(2000).build(new CacheLoader<Long, String>() {
@Override
@Nonnull
public String load(@Nonnull Long key) throws Exception {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
}
@Override
@Nonnull
public ListenableFuture<String> reload(@Nonnull Long key, @Nonnull String oldValue) throws Exception {
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
});
executorService.submit(futureTask);
return futureTask;
}
});
log.info("cache get");
String rs = cache.get(10L);
log.info("cache rs:{}", rs);
Thread.sleep(6000);
log.info("cache get");
rs = cache.get(10L);
log.info("cache rs:{}", rs);
Thread.sleep(3000);
log.info("cache get");
rs = cache.get(10L);
log.info("cache rs:{}", rs);
}
输出结果,从打印的时间可以看出,第二次不同步等待结果,获取旧值,第三次获取了第二次提交的异步任务的值
15:41:45.194 [main] INFO cache.LoadingCacheTest - cache get
15:41:46.224 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:46
15:41:52.230 [main] INFO cache.LoadingCacheTest - cache get
15:41:52.279 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:46
15:41:55.284 [main] INFO cache.LoadingCacheTest - cache get
15:41:55.284 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:53
3.2.4 更加优雅的写法
如果觉的上面的写法比较啰嗦,可以这样写,效果一样
CacheLoader<Long, String> cacheLoader = CacheLoader.asyncReloading(new CacheLoader<Long, String>() {
@Override
@Nonnull
public String load(@Nonnull Long key) throws Exception {
Thread.sleep(1000);
return DATE_FORMATER.format(Instant.now());
}
}, executorService);
LoadingCache<Long, String> cache
// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
// 设置并发级别为3,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(3)
// 过期
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 初始容量
.initialCapacity(1000)
// 最大容量,超过LRU
.maximumSize(2000).build(cacheLoader);
refreshAfterWrite的缺点:到了指定时间不过期,而是延迟到下一次查询,所以数据有可能过期了很久(假如这一段时间一直没有查询)
所以可以使用efreshAfterWrite和expireAfterWrite配合使用:
比如说控制缓存每1s进行refresh,如果超过2s没有访问,那么则让缓存失效,下次访问时不会得到旧值,而是必须得待新值加载
4. 【总结】异步加载缓存是可行的
最终我们使用了LoadingCache的refreshAfterWrite加线程池的方法实现了异步加载缓存数据,并且没有阻塞用户的线程
- 这种方法类似CopyOnWrite,在写操作的同时复制一份,读的时候先使用旧值
不过这种做法也有缺点,会导致缓存数据不是最新的,最新数据会延迟到下次查询之后的查询,需要根据场景综合考虑
参考
[1] Github Guava Doc
[2] 深入理解guava-cache的refresh和expire刷新机制
Guava LoadingCache本地缓存的正确使用姿势——异步加载的更多相关文章
- Guava LoadingCache不能缓存null值
测试的时候发现项目中的LoadingCache没有刷新,但是明明调用了refresh方法了.后来发现LoadingCache是不支持缓存null值的,如果load回调方法返回null,则在get的时候 ...
- android异步加载图片并缓存到本地实现方法
图片过多造成内存溢出,这个是最不容易解决的,要想一些好的缓存策略,比如大图片使用LRU缓存策略或懒加载缓存策略.今天首先介绍一下本地缓存图片 在android项目中访问网络图片是非常普遍性的事 ...
- Unity+NGUI打造网络图片异步加载和本地缓存工具(一)
我们已经开发了在移动终端中,异步网络图片被装入多,在unity其中尽管AssetBundle存在,通常第一个好游戏的资源,然后加载到现场,但也有很多地方可以使用异步网络加载图像以及其缓存机制. 我也写 ...
- H5 缓存机制浅析 移动端 Web 加载性能优化
腾讯Bugly特约作者:贺辉超 1 H5 缓存机制介绍 H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性.离线存储(也可称为缓存机制)是其中一个非常重要的特性.H5 引入的离线存储, ...
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
Android 高手进阶(21) 版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请注明出处http://blog.csdn.net/xiaanming/article/details ...
- [置顶] 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
转载请注明出处http://blog.csdn.net/xiaanming/article/details/9825113 异步加载图片的例子,网上也比较多,大部分用了HashMap<Strin ...
- android ListView异步加载图片(双缓存)
首先声明,参考博客地址:http://www.iteye.com/topic/685986 对于ListView,相信很多人都很熟悉,因为确实太常见了,所以,做的用户体验更好,就成了我们的追求... ...
- Android批量图片加载经典系列——使用xutil框架缓存、异步加载网络图片
一.问题描述 为提高图片加载的效率,需要对图片的采用缓存和异步加载策略,编码相对比较复杂,实际上有一些优秀的框架提供了解决方案,比如近期在git上比较活跃的xutil框架 Xutil框架提供了四大模块 ...
- Android图片管理组件(双缓存+异步加载)
转自:http://www.oschina.net/code/snippet_219356_18887?p=3#comments ImageManager2这个类具有异步从网络下载图片,从sd读取本地 ...
- [翻译]Bitmap的异步加载和缓存
内容概述 [翻译]开发文档:android Bitmap的高效使用 本文内容来自开发文档"Traning > Displaying Bitmaps Efficiently", ...
随机推荐
- 安装 Helm3 管理 Kubernetes 应用
文章转载自:http://www.mydlq.club/article/51/ 系统环境: Helm 版本:v3.5.0 Kubernetes 版本:v1.18.2 一.Helm 介绍 Helm 是一 ...
- 车辆稳定性辅助(VSA)系统
当车辆转弯大于或小于预期时,VSA有助于在转弯时稳定车辆. 同时还有助于在湿滑路面上保持牵引力. VSA 打开和关闭 禁用: 请长按按钮直至听到哔的一声.VSA 关闭指示灯点亮. 恢复: 按下按钮直至 ...
- k8s中pod的容器日志查看命令
如果容器已经崩溃停止,您可以仍然使用 kubectl logs --previous 获取该容器的日志,只不过需要添加参数 --previous. 如果 Pod 中包含多个容器,而您想要看其中某一个容 ...
- jumpserver 2222端口的使用
可以这样理解: 通过在jumpserver的web界面添加的用户,相应的也有权限通过远程使用命令的方式登陆jumpserver,进行相应的管理,只不过使用的端口是2222端口,不是常见的22端口. 一 ...
- 跟我学Python图像处理丨带你掌握傅里叶变换原理及实现
摘要:傅里叶变换主要是将时间域上的信号转变为频率域上的信号,用来进行图像除噪.图像增强等处理. 本文分享自华为云社区<[Python图像处理] 二十二.Python图像傅里叶变换原理及实现> ...
- 内网横向渗透 之 ATT&CK系列一 之 横向渗透域主机
前言 上一篇文章中已获取了关于域的一些基本信息,在这里再整理一下,不知道信息收集的小伙伴可以看回上一篇文章哦 域:god.org 域控 windows server 2008:OWA,192.168. ...
- opencv cv.line
''' 本次来学习基于opencv进行各种画图操作,以前只习惯用matplotlib,最近开始用opencv,觉得也很好用. cv.line(), cv.circle() , cv.rectangle ...
- C#/VB.NET 读取条码类型及条码在图片中的坐标位置
我们在创建条形码时,如果以图片的方式将创建好的条码保存到指定文件夹路径,可以在程序中直接加载图片使用:已生成的条码图片,需要通过读取图片中的条码信息,如条码类型.条码绘制区域在图片中的四个顶点坐标位置 ...
- 备份 MySQL 的 shell 脚本(mysqldump版本) shell脚本
#!/bin/bash # 备份 MySQL 的 shell 脚本(mysqldump版本) # 定义变量 user(数据库用户名),passwd(数据库密码),date(备份的时间标签) # dbn ...
- Ubuntu 环境下安装 Docker
系统要求 Docker目前只能运行在64位平台上,并且要求内核版本不低于3.10,实际上内核越新越好,过低的内核版本容易造成功能不稳定. 用户可以通过如下命令检查自己的内核版本详细信息: $ unam ...