缓存使用场景
计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
缓存类型
- 分布式缓存
- 本地缓存(local cache) / 内存缓存(memory cache)
Local Cache
- Google guava cache: 内存缓存框架
- Encache: 不仅仅是内存缓存,还支持硬盘缓存
- Oscache: 常用来缓存页面
Google Guava Cache
优点
- 线程安全的本地缓存
- 很好的封装了get、put操作,能够集成数据源。
- 过期时间,元素失效
- 回收机制
- 监控缓存加载/命中等
适用场景
- 多线程下内存缓存
内存结构
类似于ConcurrentHashMap
- 队列用于缓存淘汰机制,适用LRU(最近最久未使用)最先淘汰。
- ReferenceEntry 用于保存key-value
常用方法
创建
- CacheLoader
LoadingCache<String, Employee> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(30L, TimeUnit.MILLISECONDS)
.build(createCacheLoader());
- Callable callback
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build();
- 回收的参数
- 大小的设置:
- CacheBuilder.maximumSize(long)
- CacheBuilder.weigher(Weigher)
- CacheBuilder.maxumumWeigher(long)
- 时间
- expireAfterAccess
- expireAfterWrite
- 引用
- CacheBuilder.weakKeys()
- CacheBuilder.weakValues()
- CacheBuilder.softValues()
- 明确删除
- invalidate(key)
- invalidateAll(keys)
- invalidateAll()
- 删除监听器
- CacheBuilder.removalListener(RemovalListener)
- 大小的设置:
加载
- 自动加载
- 原则:“获取缓存-如果没有-则计算”[get-if-absent-compute]
- 严格限制只有1个加载操作,防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应
- CacheLoader: 为整个cache设置统一的值来源
- Callable callback:在get是指定callable方法,为不同的key指定不同的value方法。
// CacheLoader
LoadingCache<String, Employee> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(30L, TimeUnit.MILLISECONDS)
.build(createCacheLoader());
public static com.google.common.cache.CacheLoader<String, Employee> createCacheLoader() {
return new com.google.common.cache.CacheLoader<String, Employee>() {
@Override
public Employee load(String key) throws Exception {
....
}
};
}
// Callable
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build();
// 如果有,则返回;没有则调用callable方法取得value,写入缓存,然后返回。
try {
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
...
}
- 手动加载/手动插入
- Cache.put(key, value)
缓存回收
- 被动移除
- 基于容量的回收:
- CacheBuilder.maximumSize(long),接近最大条数,按照LRU移除最早的。
- 定时回收
- 根据某个键值对最后一次访问之后多少时间后移除:expireAfterAccess
- 根据某个键值对被创建或值被替换后多少时间移除:expireAfterWrite
- 基于引用回收
- java的垃圾回收机制
- 基于容量的回收:
- 主动移除/显示移除
- 单独移除: Cache.invalidate(key)
- 批量移除: Cache.invalidateAll(keys)
- 移除所有: Cache.invalidateAll()
清理什么时候发生?
CacheBuilder构建的缓存不会"自动"执行清理和回收工作,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。
如果缓存是高吞吐的,无需担心缓存的维护和清理等工作。如果缓存吞吐比较少,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以实现定时调度。
刷新
刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值。
如果刷新过程抛出异常,缓存将保留旧值。
- LoadingCache.refresh(K) 在生成新的value的时候,旧的value依然会被使用。
- CacheLoader.reload(K, V) 生成新的value过程中允许使用旧的value
- 定时刷新可以让缓存项保持可用,缓存项只有在被检索时才会真正刷新CacheBuilder.refreshAfterWrite(long, TimeUnit)
Note: 更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。
过期
- expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。
- expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。
- refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。
统计
- CacheBuilder.recordStats()用来开启Guava Cache的统计功能。
- Cache.stats()方法会返回CacheStats对象以提供如下统计信息:
- hitRate():缓存命中率;
- averageLoadPenalty():加载新值的平均时间,单位为纳秒;
- evictionCount():缓存项被回收的总数,不包括显式清除。
移除监听器
缓存项被移除时的额外操作可以使用移除监听器。默认情况下,监听器方法是在移除缓存时同步调用的。为了防止同步模式下会拖慢正常的缓存请求,可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。
获取cache
对于get(key, loader)方法流程:
- 对key做hash,找到存储的segment及数组table上的位置;
- 链表上查找entry,如果entry不为空,且value没有过期,则返回value,并刷新entry。
- 若链表上找不到entry,或者value已经过期,则调用lockedGetOrLoad。
- 锁住整个segment,遍历entry可能在的链表,查看数据是否存在是否过期,若存在则返回。若过期则删除(table,各种queue)。若不存在,则新建一个entry插入table。放开整个segment的锁。
- 锁住entry,调用loader的reload方法,从数据源加载数据,然后调用storeLoadedValue更新缓存。
- storeLoadedValue时,锁住整个segment,将value设置到entry中,并设置相关数据(入写入/访问队列,加载/命中数据等)。
附录
源码解析:http://www.cnblogs.com/java-zhao/p/5140878.html
Refresh和expire解析:https://blog.youkuaiyun.com/lzwglory/article/details/82747482