前言
在大部分互联网架构中 Cache 已经成为了必可不少的一环。常用的方案有大家熟知的 NoSQL 数据库(Redis、Memcached),也有大量的进程内缓存比如 EhCache 、Guava Cache、Caffeine 等。
本讲主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析,会选取本地缓存和分布式缓存(NoSQL)的优秀框架比较他们各自的优缺点、应用场景、项目中的最佳实践以及原理分析。
一、Guava Cache介绍
Guava Cache 是 google 开源的一款本地缓存工具库,它的设计灵感来源于ConcurrentHashMap,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
传统的JVM 缓存,是堆缓存,其实就是创建一些全局容器,比如:List、Set、Map等。这些容器用来做数据存储还可以,却不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等,也没有清除数据时的回调通知,而且多线程不安全。虽然针对高并发可以使用CurrentHashMap,但是过期处理和数据刷新都需要手动完成。
相比较而言,同样是基于 JVM 缓存的 Guava Cache 就显得优势明显,且很有必要:
1. 缓存过期和淘汰机制
在Guava Cache中可以设置Key的过期时间,包括访问过期和创建过期Guava Cache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除;
2. 并发处理能力
Guava Cache类似CurrentHashMap,是线程安全的。它提供了设置并发级别的API,使得缓存支持并发的写入和读取;
像ConcurrentHashMap结构类似,GuavaCache也使用Segment做分区,采用分离锁机制,分离锁能够减小锁力度,提升并发能力分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。
3. 更新锁定
一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降 Guava Cache 可以在 CacheLoader 的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。
4. 集成数据源
一般我们在业务中操作缓存,都会操作缓存和数据源两部分。Guava Cache 的 get 可以集成数据源,在从缓存中读取不到时,可以从数据源中读取数据并回填缓存。
5. 监控缓存加载/命中情况
既然是缓存服务,那统计数据的功能自然也是少不了。
二、基本用法
Guava Cache 的 maven 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
2.1 构建Cache对象
Guava Cache 通过简单好用的 Client 可以快速构造出符合需求的 Cache 对象,不需要过多复杂的配置,大多数情况就像构造一个 POJO 一样的简单。
这里介绍两种构造 Cache 对象的方式:CacheLoader 和 Callable。
2.1.1 CacheLoader
构造 LoadingCache 的关键在于实现 load 方法,也就是在需要访问的缓存项不存在的时候 Cache 会自动调用 load 方法将数据加载到 Cache 中。这里你肯定会想假如有多个线程过来访问这个不存在的缓存项怎么办,也就是缓存的并发问题如何怎么处理是否需要人工介入,这些在下文中也会介绍到。
除了实现 load 方法之外还可以配置缓存相关的一些性质,比如:过期加载策略、刷新策略 。
private static final LoadingCache<String, String> CACHE = CacheBuilder
.newBuilder()
// 最大容量为 100 超过容量有对应的淘汰机制,下文详述
.maximumSize(100)
// 缓存项写入后多久过期,下文详述
.expireAfterWrite(60 * 5, TimeUnit.SECONDS)
// 缓存写入后多久自动刷新一次,下文详述
.refreshAfterWrite(60, TimeUnit.SECONDS)
// 创建一个 CacheLoader,load 表示缓存不存在的时候加载到缓存并返回
.build(new CacheLoader<String, String>() {
// 加载缓存数据的方法
@Override
public String load(Stri