关于Google的Cache使用
因博主最近需要使用本地缓存保存常用但不重要的数据,查询资料后,Google的Guava很适合,记录一下.
GitHub官网:
https://github.com/google/guava/wiki/CachesExplained
中文资料:
https://ifeve.com/google-guava/
参考资料:
https://www.jianshu.com/p/afe7b2dccee0
https://my.oschina.net/chkui/blog/726442
https://www.bbsmax.com/A/MyJxP0jXJn/
1Cache的概述
Guava 是由Google开发的基于Java的开源库,包含许多Google核心库,它有助于最佳编码实践,并有助于减少编码错误。它为集合 collections
、缓存 caching
、原生类型支持 primitives support]
、并发库 concurrency libraries
、通用注解 common annotations
、字符串处理 string processing
、I/O 等等提供实用程序方法.
Cache是Google的Guava当中的缓存部分功能.
其具备了很多优秀的特点:
- 数据写入缓存时是原子操作.
- 缓存的数据达到最大规模时,会使用“最近最少使用(LRU)”算法来清除缓存数据.
- 每一条数据还可以基于时间回收,未使用时间超过一定时间后,数据会被回收.
- 缓存被清除时,可发送通知告知
- 提供访问统计功能
2 官方文档
官方范例:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
new CacheLoader<Key, Graph>() {
@Override
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。
使用范围:
- 消耗内存提升访问速度
- 访问的数据会被多次使用
- 缓存的数据量不大,因为是基于内存保存数据,且仅仅支持单体应用.
1 加载
CacheLoader
LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
...
try {
return graphs.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
从LoadingCache查询的正规方式是使用get(K)
方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
Callable
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)
方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回.
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
显式插入
使用cache.put(key, value)
方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值.
2 缓存回收
Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收.
1 基于容量的回收(size-based eviction)
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
,缓存将尝试回收最近没有使用或总体上很少使用的缓存项。
在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时.
缓存的权重:
不同的缓存项有不同的“权重”(weights),如果你的缓存值,占据完全不同的内存空间,你可以使用``CacheBuilder.weigher(Weigher)指定一个权重函数,并且用
CacheBuilder.maximumWeight(long)`指定最大总重.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) {
// no checked exception
return createExpensiveGraph(key);
}
});
2 定时回收(Timed Eviction)
提供了两种定时回收策略:
expireAfterAccess(long, TimeUnit)
缓存项在给定时间内没有被读/写访问,则回收.expireAfterWrite(long, TimeUnit)
缓存项在给定时间内没有被写访问(创建或覆盖),则回收.
3 基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys()
使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收.(因为垃圾回收仅依赖恒等式(==
),使用弱引用键的缓存用==而不是equals比较键)CacheBuilder.weakValues()
使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收.(因为垃圾回收仅依赖恒等式(==
),使用弱引用键的缓存用==而不是equals比较键)CacheBuilder.softValues()
使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收.(使用软引用值的缓存同样用==而不是equals比较值)
4 显式回收
可以手动显式清楚缓存项,即通过代码完成清除功能.
清除方式:
- 单个清除
Cache.invalidate(key)
- 批量清除
Cache.invalidateAll(keys)
- 清除所有缓存
Cache.invalidateAll()
5 监听器
通过CacheBuilder.removalListener(RemovalListener)
,可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener
会获取移除通知RemovalNotification
,其中包含移除原因RemovalCause
、键和值.
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};
return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。可以使用
RemovalListeners.asynchronous(RemovalListener, Executor)
把监听器装饰为异步操作.
6 什么时候清理?
使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话.
如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了.
如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()
和ScheduledExecutorService
可以帮助你很好地实现这样的定时调度
7 刷新
刷新和回收不太一样。正如LoadingCache.refresh(K)
所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成.
// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) {
// no checked exception
return getGraphFromDatabase(key);
}
public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prevGraph);
} else {
// asynchronous!
ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
public Graph call() {
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
CacheBuilder.refreshAfterWrite(long, TimeUnit)
可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用.
8 其他特性
1 统计
CacheBuilder.recordStats()
用来开启Guava Cache的统计功能.
Cache.stats()
方法会返回CacheStats
对象以提供如下统计信息:
hitRate()
缓存命中率averageLoadPenalty()
加载新值的平均时间,单位为纳秒evictionCount()
缓存项被回收的总数,不包括显式清除
2 asMap视图
asMap视图提供了缓存的ConcurrentMap形式.
- cache.asMap()包含当前所有加载到缓存的项.
- asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载.
- 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作.
3 中断
缓存加载方法(如Cache.get)不会抛出InterruptedException.
Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力.
建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象.
3 使用方法
使用是只需要导入maven依赖即可
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
范例1:
使用Guava构建本地缓存,在拦截器中,保存接口的调用信息,做接口一定时间内重复调用的校验.
@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
// 缓存方案
// 方案1 使用redis保存该ip在一定时间内调用接口的次数 (适用集群)
// @Autowired
// private RedisTemplate redisTemplate;
public static final int IntervalTime = 8;
// 方案2 谷歌的guava , 使用本地缓存,设置有效期8秒 (适用单体)
private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterAccess(IntervalTime, TimeUnit.SECONDS).build();
/**
* 请求处理前调用
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("========================进入拦截器1=================================");
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 查看该方法上是否有需要拦截的注释
MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class);
// 注解存在
if (myAnnotation != null) {
int maxCount = myAnnotation.maxCount();
int seconds = myAnnotation.seconds();
// 1 获取ip ip可能代理
// 代理服务器在请求转发时添加上去的
String ip = request.getHeader("x-forwarded-for");
log.info("x-forwarded-for = {} ", ip);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP = {} ", ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP = {} ", ip);
}
// remote_addr http协议传输的时候自动添加,不受请求头header的控制
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("remote_addr = {} ", ip);
}
// 缓存方案1 redis
// if (redisCache(response, maxCount, seconds, ip)) {return false;}
// 缓存方案2 Google的guava
if (googleCache(response, maxCount, seconds, ip)) {
return false;}
}
// 不拦截,进入下一个拦截器
return true;
}
/**
* 使用Google的guava
* @param response
* @param maxCount
* @param seconds
* @param ip
* @return
* @throws IOException
*/
private boolean googleCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException, ExecutionException {
// 2 从缓存中获取该ip的访问次数
Integer count = cache.getIfPresent(ip);
// 3 判断是否满足注解设置要求
if (count == null) {
cache.put(ip,1);
} else if (count < maxCount) {
// 3.2 在有效期内,访问次数满足要求
cache.put(ip,++count);
} else {
// 3.3 超过最大访问次数,拒绝该请求
log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ResultResponse resultResponse = new ResultResponse();
resultResponse.setResult("操作太快,请稍后访问系统");
Object obj = JSONObject.toJSON(resultResponse);
response.getWriter().write(JSONObject.toJSONString(obj));
return true;
}
return false;
}
/**
* 使用redis作为缓存
* @param response
* @param maxCount
* @param seconds
* @param ip
* @return
* @throws IOException
*/
/* private boolean redisCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException {
// 2 从redis中获取该ip的访问次数
Integer count = (Integer) redisTemplate.opsForValue().get(ip);
// 3 判断是否满足注解设置要求
if (count == null) {
// 3.1 第一次访问,设置次数,有效期
redisTemplate.opsForValue().set(ip, 1);
redisTemplate.expire(ip, seconds, TimeUnit.SECONDS);
} else if (count < maxCount) {
// 3.2 在有效期内,访问次数满足要求
redisTemplate.opsForValue().set(ip, ++count);
} else {
// 3.3 超过最大访问次数,拒绝该请求
log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ResultResponse resultResponse = new ResultResponse();
resultResponse.setResult("操作太快,请稍后访问系统");
Object obj = JSONObject.toJSON(resultResponse);
response.getWriter().write(JSONObject.toJSONString(obj));
return true;
}
return false;
}*/
}
范例2
设置一个有效期5秒的缓存cache,当访问不存在的key,则查询数据库,缓存数据.当key存在,直接返回.在有效期过后,会自动删除key.
public class GoogleGuavaCacheTest {
private final LoadingCache<String, String> cache;
public GoogleGuavaCacheTest() {
/**
* 设置5秒自动过期
*/
cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
public String load(String id) throws Exception {
System.out.println("method inovke");
//这里执行查询数据库,等其他复杂的逻辑
return "User:" + id;
}
});
}
public String getAndyName(String id) throws Exception {
return cache.get(id);
}
}
// 测试
class GuavaCacheTest {
public static void main(String[] args) throws Exception {
GoogleGuavaCacheTest us = new GoogleGuavaCacheTest();
for (int i = 0; i < 20; i++) {
System.out.println(us.getAndyName("6666"));
TimeUnit.SECONDS.sleep(1);
}
}
}