学习目标:
1、了解缓存的作用
2、了解Guava对缓存的操作方法
学习过程:
一、为什么需要缓存
缓存需要消耗一些内存空间达到提升速度的功能。如果一些数据需要多次的访问,缓存起来效率会高很多,但是也要注意所以如果一条数据不需要多次的访问,也就没有缓存起来,因为这样会消耗内容,还有就是缓存存放的数据总量不会超出内存容量,如果大量占用了内存也会导致系统变慢的。
还需要考虑的就是缓存放到哪里?一般可以分为本地缓存和分布式缓存两种方式。本地缓存就只能在本地访问,在需要缓存量不是很大的时候,同时对缓存的数据也不是很重要的情况下,本地缓存操作方便,但是如果缓存量很大,为了提供内存的利用率,提高缓存的稳定性,可以采用分布式缓存的方案。
本地缓存,如果是web服务呢,我们经常使用的session,session当然也是一种非常好用的缓存,但是只针对某个用户的,今天我们将会学习google的Guava的包封装的一个缓存工具类。
二、Guava的缓存封装工具
平时我们查询数据库时,并没有把数据缓存起来,如果需要缓存,我们一般的代码思路
(1)、先判断缓存是否有此数据
(2)、如果有直接从缓存取值
(3)、如果没有就查询数据,并把数据放到缓存中
这样模板式的处理比较死板,Guava帮我们封装一下,就不需要写得这么复杂了。还需要考虑的就是缓存时效、最大限制、缓存的清空等。Guava比较都封装得比较好了。
1、基本使用
导入包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>14.0.1</version>
</dependency>
使用代码:
@Service
public class GoodService {
@Autowired
private GoodsDao goodsDao;
private final static String PREFIX="good:";
LoadingCache<String, Goods> goodsCache = CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterAccess(20, TimeUnit.SECONDS).build(new CacheLoader<String, Goods>() {
@Override
public Goods load(String goodId) throws Exception {
Goods good = goodsDao.queryByid(Integer.valueOf(goodId.split(":")[1]));
if (good == null) {
good = new Goods();
}
return good;
}
});
public Goods getGoodCache(int goodId) {
Goods goods=null;
try {
goods= goodsCache.get(PREFIX+goodId);
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return goods;
}
/**
* 手动清除
* @param goodId
* @return
*/
public void clearGoodCache(int goodId) {
goodsCache.invalidate(PREFIX+goodId);
}
public void clearAllGoodCache() {
goodsCache.invalidateAll();
}
}
测试代码,你可以再中间清空缓存得操作注释和不注释得时候得区别。
@RunWith(SpringJUnit4ClassRunner.class) // 使用junit4进行测试
@ContextConfiguration(locations = { "classpath:applicationContext.xml" }) // 加载配置文件
public class GoodServiceTest {
@Autowired
private GoodService goodService;
@Test
public void testGetUserCache() {
Goods good=goodService.getGoodCache(9);
//goodService.clearGoodCache(good.getGoodsId());
Goods good1=goodService.getGoodCache(9);
System.out.println(good.getGoodsName());
System.out.println(good1.getGoodsName());
}
}
三、缓存回收
什么时候某个缓存项就不值得保留了?Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
1、基于容量的回收(size-based eviction)
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。比如我可以设置价钱越高权重越高。
2、定时回收(Timed Eviction)
CacheBuilder提供两种定时回收的方法:
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
定时回收周期性地在写操作中执行,偶尔在读操作中执行。代码修改如下:
LoadingCache<String, Goods> goodsCache = CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterAccess(20, TimeUnit.SECONDS).weigher(new Weigher<String, Goods>() {
public int weigh(String key, Goods value) {
return ((Double)value.getGoodsCash()).intValue();
}
}).build(new CacheLoader<String, Goods>() {
@Override
public Goods load(String goodId) throws Exception {
Goods good = goodsDao.queryByid(Integer.valueOf(goodId.split(":")[1]));
if (good == null) {
good = new Goods();
}
return good;
}
});
public Goods getGoodCache(int goodId) {
Goods goods = null;
try {
goods = goodsCache.get(PREFIX + goodId);
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return goods;
}
3、基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
4、显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
四、清除什么时候发生
也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
这在GuavaCache被称为“延迟删除”,即删除总是发生得比较“晚”,这也是GuavaCache不同于其他Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动线程,不管是实现,还是使用起来都让人觉得简单(轻量)。
如果你还是希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。