Java开发利器之Guava Cache的使用教程
作者:指北君 发布时间:2022-03-20 19:22:02
前言
缓存技术被认为是减轻服务器负载、降低网络拥塞、增强Web可扩展性的有效途径之一,其基本思想是利用客户访问的时间局部性(Temproral Locality)原理, 将客户访问过的内容在Cache中存放一个副本,当该内容下次被访问时,不必连接到驻留网站,而是由Cache中保留的副本提供。
在企业Web应用中,通过缓存技术能够提高请求的响应速度;减少系统IO开销;降低系统数据读写压力...
缓存的意义
首先我们要知道,在我们开发过程中,为什么要使用缓存,缓存能够为我们带来哪些好处!
优点
通过缓存承载系统压力,减少对系统或网络资源访问而引起的性能消耗,在流量较大时能够很好地减少系统拥塞
缓存一般都是使用存取非常快的组件实现,通过缓存能够快速响应客户端请求,从而降低客户访问延迟,提审系统响应速度
在配备负载均衡的应用架构中,通过缓存静态资源能够有效减少服务器负载压力
当下游应用故障时,通过返回缓存数据能够在一定程度上增强应用容错性
缺点
缓存数据与实际数据不一致问题问题
高并发场景时存在缓存击穿、缓存穿透、缓存雪崩等问题
总的来说,缓存主要是针对高频访问但低频更新的数据,从而加快服务器响应与原资源访问压力
Guava Cache是一个相对比较简单并且容易理解的本地缓存框架,今天主要以此为开端来认识并学习如何使用缓存
Guava Cache特色
本地缓存我们可以简单的理解为Map,将数据保存到Map(内存)中,下次使用该数据时,通过key直接从Map中取即可。但是使用Map会有一些几个问题需要考虑:
缓存的容量。不可能无限制的对数据进行缓存,当数据较大时占用系统资源会导致主业务受影响
缓存的清理。有些缓存使用频率很低,如果一直占用资源也是一种浪费
并发访问时的效率问题。缓存更新时瞬时对系统、网络资源的访问导致故障
缓存使用情况评估
当然以上问题我们通过我们对Map包装下即可实现,当然Guava Cache也就是基于这种思想,底层原理则是基于Map实现,我们看下其有哪些特色:
缓存过期和淘汰机制
通过设置Key的过期时间,包括访问过期和创建过期;设置缓存容量大小,采用LRU的方式,选择最近最久的缓存进行删除。
并发处理能力
Cache主要基于CurrentHashMap实现线程安全;通过对key的计算,基于分段锁,提高缓存读写效率,降低锁的粒度,提升并发能力
更新锁定
在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。在高并发下会出现,多次查询元数据并重复回填缓存,可能会造成系统故障,最明显的DB服务器宕机,性能下降等。GuavaCache通过在CacheLoader调用load方法时,对同一个key同一时刻只会有一个请求去读源数据并回填缓存,后面的请求则直接继续从缓存读取,有效阻断并发请求对资源服务的影响。
集成数据源
一般我们在业务中操作缓存,都会操作缓存和数据源两部分GuavaCache的get可以集成数据源,在从缓存中读取不到时可以从数据源中读取数据并回填缓存
监控统计
监控缓存加载次数、命中率、失误率以及数据加载时长等
API介绍
1.缓存构建
ManualCache 此时Cache相当于一个Map,对数据进行CRUD操作时,需要同步操作缓存Map; 高并 * 况时,可以使用get(k,loader)读缓存,通过Cache锁机制,防止对系统资源(DB)的并发访问 通过put方法实现缓存的存入与更新;
LoadingCache 此时构建的是一个实现了Cache接口的LoadingCache,相比ManualCache,提供了缓存回填机制,即当缓存不存在时,会基于CacheLoader查询数据并将结果回填到缓存, 在高并发时,可以有效地基于缓存锁减少对系统资源的调用。此时仅需要关注缓存的使用,缓存的更新与存入都是基于CacheLoader实现;
2.缓存获取
get(k) 根据key查询,没有则触发load;如果load为空则抛出异常
getUnchecked(k) 缓存不存在或返回为null会抛出检查异常
get(k,loader) 根据key查询,没有则调用loader方法,且对结果缓存;如果loader返回null则抛出异常,此时不会调用默认的load方法
getIfPresent(k) 有缓存则返回,否则返回null,不会触发load
3.缓存更新
put(k,v) 如果缓存已经存在,则会先进行一次删除
4.缓存删除
invalidate(k) 根据key使缓存失效
过期 通过配置的过期参数,比如expireAfterAccess、expireAfterWrite、refreshAfterWrite
过载 当缓存数据量超过设置的最大值时,根据LRU算法进行删除
引用 构建缓存时将键值设置为弱引用、软引用,基于GC机制来清理缓存
5.统计
hitRate() 缓存命中率;
hitMiss() 缓存失误率;
loadCount() 加载次数;
averageLoadPenalty() 加载新值的平均时间,单位为纳秒;
evictionCount() 缓存项被回收的总数,不包括显式清除。
Builder配置
配置 | 描述 |
---|---|
expireAfterAccess | 多久没有读写则过期 |
expireAfterWrite | 写入后多久没更新自动过期,先删除,后load |
refreshAfterWrite | 上一次更新后多久自动刷新,先reload后删除,并发时会取到老的数据 |
removalListener | 设置缓存删除监听 |
initialCapacity | 缓存初始化大小 |
concurrencyLevel | 最大的并发数,可以理解为并发线程数量 |
maximumSize | 最大缓存数量,超过时会根据策略清除 |
maximumWeight | 最大权重容量数,仅用于确定缓存是否超过容量 |
recordStats | 缓存命中统计 |
简单示例
ManualCache模式
下面以用户服务为例,我们看下如何在增删改查方法中使用缓存:
private Cache<String, User> cache = CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)//写入多久没更新自动过期,先删除,后load
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
LOGGER.info("{} remove {}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),notification.getKey());
}
})
.initialCapacity(20) //初始化容量
.concurrencyLevel(10) // 并发
.maximumSize(100) //最多缓存数量
.recordStats() // 开启统计
.build();
@Override
public User getUser(String id){
// 缓存不存在时,通过LocalCache锁机制,防止对数据库的高频访问
User user;
try {
user = cache.get(id,()-> {
LOGGER.info("缓存不存在,从loader加载数据");
return userDao.get(id);
});
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
return user;
}
@Override
public User saveOrUpdateUser(User user){
userDao.saveOrUpdate(user);
cache.put(user.getId(),user);
return user;
}
@Override
public void removeUser(String id){
userDao.remove(id);
cache.invalidate(id);
}
LoadingCache模式
private LoadingCache<String, User> cache = CacheBuilder.newBuilder()
// 省略
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
LOGGER.info("{} load {}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),key);
return userDao.get(key);
}
@Override
public ListenableFuture<User> reload(String key, User oldUser) throws Exception {
LOGGER.info("{} reload {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),key);
ListenableFutureTask<User> listenableFutureTask = ListenableFutureTask.create(() -> userDao.get(key));
CompletableFuture.runAsync(listenableFutureTask);
return listenableFutureTask;
}
});
@SneakyThrows
@Override
public User getUser(String id){
// 缓存不存在或返回为null会抛出异常
try {
return cache.getUnchecked(id);
} catch (Exception e) {
return null;
}
}
@Override
public User saveOrUpdateUser(User user){
cache.invalidate(user.getId());
return userDao.saveOrUpdate(user);
}
@Override
public void removeUser(String id){
cache.invalidate(id);
userDao.remove(id);
}
总结:第一种写法更像是前面说到的Map,在对数据进行CRUD操作时,需要用户手动对缓存进行同步的更新或删除操作,所以叫ManualCache(手动),当然Guava Cache对Map的加强依然有效,比如过期清除,缓存容量限制。第二种方式写法差不多,主要是引入了CacheLoader接口,在读数据时缓存数据不存在时,通过CacheLoader的load方法先写缓存后返回数据
注意
1.expireAfterWrite、refreshAfterWrite的区别
在refreshAfterWrite导致缓存失效时,并不会因为更新缓存而阻塞缓存数据的返回,只不过是返回老的数据
2.不能缓存null
有时候为了将值为null的数据统一缓存,这样就不会因为没有缓存数据而访问数据库造成压力
3.读写时才进行删除
Guava Cache的缓存数据删除是在更新或写入时才会触发,没有单独的调度服务完成这一工作
本地缓存
类似的本地缓存还有,有兴趣的可以自己尝试,其实实现思想应该也差不多
来源:https://mp.weixin.qq.com/s/oSwIQT5TQykiOFmKlyMfvw
猜你喜欢
- 项目需求最近项目中有一个需求就是让Java代码去代替人工操作,自动生成PPT,具体就是查询数据库数据,然后根据模板文件(PPT),将数据库数
- C#编程语言非常优美,我个人还是非常赞同的。特别是在学习一段时间C#后发现确实在它的语法和美观度来说确实要比其它编程语言强一些(也可能是由于
- 背景介绍在开发应用过程中经常会遇到显示一些不同的字体风格的信息犹如默认的LockScreen上面的时间和充电信息。对于类似的情况,可能第一反
- 同步是一种只允许一个线程在特定时间访问某些资源的技术。没有其他线程可以中断,直到所分配的线程或当前访问线程访问数据完成其任务。在多线程程序中
- 微信公众平台(map.weixin.qq.com)/开放平台(open.weixin.qq.com)/商户平台(pay.weixin.qq.
- Knn算法的核心思想是如果一个样本在特征空间中的K个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性
- 正文将 Java 对象序列化为二进制文件的 Java 序列化技术是 Java 系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要
- Java多线程下载网图案例此案例依赖——文件操作工具类(FileUtils)使用 apache 的commons-io包下的FileUtil
- 水波纹效果已经不是什么稀罕的东西了,用过5.0新控件的小伙伴都知道这个效果,可是如果使用一个TextView或者Button或者其它普通控件
- 一直使用Eclipse环境开发Android,也尝鲜使用过Android Studio去开发,各种IDE配合Android SDK及SDK原
- 前言平时日常开发用得最多是Http通讯,接口调试也比较简单的,也有比较强大的框架支持(OkHttp)。个人平时用到socket通讯的地方是A
- 服务降级服务压力剧增的时候,根据当前的业务情况及流量对一些服务和页面有策略的降级,以此缓解服务器的压力,以保证核心任务的进行。同时保证部分甚
- 对象内存分配与回收策略对象的内存分配,往大方向讲,就是在堆上分配〔但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在
- 首先不可否认,这些在面试上会经常被面试官问起,但是你回答的让面试官满意吗?当然如果你知道了这些原理,或许你就不怕了。既然说到了原理,我们还是
- 使用代码特别注意 :拼接条件时,所使用到的条件strID,strBir必须是独立的var predicate = PredicateBuil
- Eureka什么是服务治理为什么需要服务治理?  服务治理是主要针对分布式服务框架的微服务,处理服务调用
- 当我们在项目中登录使用验证码的时候,不妨试试Kaptcha生成验证码,非常简单1、首先,我们在pom.xml文件中引入kaptcha的mav
- 在学习操作系统这本书的时候,我们使用的是汤小丹老师的《计算机操作系统》接下来我将会使用java语言去实现内部代码。Swap指令最佳置换算法是
- 一、实验:继承1、概念Maven工程之间,A 工程继承 B 工程B 工程:父工程A 工程:子工程本质上是 A 工程的 pom.xml 中的配
- 通过AIDL接口在进程间传递数据,记录在开发中遇到的一写问题AIDL支持数据类型如下:1. Java 的原生类型2. String 和Cha