摘要: 学习Google内部使用的工具包Guava,在Java项目中轻松地增加缓存,提高程序获取数据的效率; 业务实现上需要用到本地缓存来解决一些数据量相对较小但是频繁访问的数据
Guava Cache适用场景:
你愿意消耗一部分内存来提升速度;
你已经预料某些值会被多次调用;
缓存数据不会超过内存总量;
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好
Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所添加的元素,直到显式的移除;Guava Cache为了限制内存的占用,通常都是设定为自动回收元素。在某些场景下,尽管LoadingCahe不回收元素,但是它还是很有用的,因为它会自动加载缓存。
依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
// 获取历史版本的频道商品列表
private LoadingCache<String, List<Bean>> beanCache;
@PostConstruct
public void init() {
beanCache = CacheBuilder.newBuilder()
//过期时间5分钟(时间段内没有更新就会被回收)
.expireAfterWrite(5 * 60, TimeUnit.SECONDS).
//缓存数量为5000个
.maximumSize(5000)
.build(new CacheLoader<String, List<Bean>>() {
@Override
public List<Bean> load(String key) throws Exception {
return this.getBeanInfo(key);
}
});
}
刷新机制:包括refresh和expire刷新机制
expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。
expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。
refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。
考虑到时效性,我们可以使用expireAfterWrite,使每次更新之后的指定时间让缓存失效,然后重新加载缓存。guava cache会严格限制只有1个加载操作,这样会很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应。
然而,通过分析源码,guava cache在限制只有1个加载操作时进行加锁,其他请求必须阻塞等待这个加载操作完成;而且,在加载完成之后,其他请求的线程会逐一获得锁,去判断是否已被加载完成,每个线程必须轮流地走一个“”获得锁,获得值,释放锁“”的过程,这样性能会有一些损耗。这里由于我们计划本地缓存1秒,所以频繁的过期和加载,锁等待等过程会让性能有较大的损耗。
refreshAfterWrite的特点是: 在refresh的过程中,严格限制只有1个重新加载操作,而其他查询先返回旧值,这样有效地可以减少等待和锁争用,所以refreshAfterWrite会比expireAfterWrite性能好。但是它也有一个缺点,因为到达指定时间后,它不能严格保证所有的查询都获取到新值。guava cache并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行加载或刷新。所以,如果使用refreshAfterWrite,在吞吐量很低的情况下,如很长一段时间内没有查询之后,发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题。
先执行postProcessBeforeInitialization,然后是afterPropertiesSet,再然后是init-method,最后是postProcessAfterInitialization。
/**
* Created with IntelliJ IDEA.
*
* @Description:
* @author: bowang
* @create: 2019-06-04 下午3:30
**/
public class AsyncCacheLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(AsyncCacheLoader.class);
public static <K, V> CacheLoader<K, V> buildAysncCacheLoader(final Function<K, V> function,
final ThreadPoolExecutor executor) {
return CacheLoader.asyncReloading(new CacheLoader<K, V>() {
@Override
public V load(K k) throws Exception {
LOGGER.info("线程{}刷新缓存", Thread.currentThread().getId());
try {
return function.apply(k);
} finally {
LOGGER.info("线程{}刷新缓存结束", Thread.currentThread().getId());
}
}
}, executor);
}
}
private LoadingCache<BeaverLinkSkuListRequest, List> shippingRegionInfoCache = CacheBuilder.newBuilder()
/** 基于容量的回收 1000 个key */
.maximumSize(10)
/** 5s */
.refreshAfterWrite(5,TimeUnit.SECONDS)
/** 初始容量 */
.initialCapacity(20)
.build(AsyncCacheLoader.buildAysncCacheLoader(this::refreshInfo, ThreadPoolUtils.executorService));
//注意key 一般都是String, Integer, 其他bean看情况重写hashcode和equals
private List refreshInfo(BeaverLinkSkuListRequest request) {
List<String> list = new ArrayList<>();
if (null != request && StringUtils.isBlank(request.getName())) {
return getBeaverLinkSkuList(request.getCommonParams(), request.getBeaverLink(), request.getLimitSize());
} else {
return getSuperBeaverLinkSkuList(request.getCommonParams(), request.getBeaverLink(), request.getLimitSize());
}
}