一 序
本文属于《深入分布式缓存 》读书笔记,第一章:缓存为王主要介绍缓存概念,以及引入缓存的背景:提升用户体验。还介绍了缓存的分类,第二章主要介绍分布式理论。个人觉得第二章可以去掉,毕竟是泛泛的介绍。还是专门去看有主题的书比较好,比如《<从PAXOS到ZOOKEEPER分布式一致性原理与实践》。第4章主要介绍EHcache。因为实际项目采用了guava +redis. 所以本文打算重点看看guava cache。
二 Ehcache
先说下官网:http://www.ehcache.org/ 貌似需要翻墙。
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
Spring 提供了对缓存功能的抽象:即允许绑定不同的缓存解决方案(如Ehcache),但本身不直接提供缓存功能的实现。它支持注解方式使用缓存,非常方便。
Ehcache的特点:
- 快速
- 简单
- 多种缓存策略
- 缓存数据有两级:内存和磁盘,因此无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过RMI、可插入API等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理器实例,以及一个实例的多个缓存区域
- 提供Hibernate的缓存实现
适用场景:
1 数据更新比较少的情况,
2 对并发、一致性要求不高的情况。本地缓存的特性,不适合解决不同服务器间缓存同步的问题,更推荐redis等集中式缓存。
优化方案:1 定时轮询。2.主动通知。
三 guava cache
先贴一下官网:https://github.com/google/guava/wiki/CachesExplained
3.1 jvm缓存
就是堆缓存。其实就是创建一些全局变量,如 Map、List 之类的容器用于存放数据。
优点:简单,占用内存小。
缺点:不具备缓存的常见操作,如:
只能显式的写入,清除数据。
不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等。
清除数据时的回调通知。
其他一些定制功能等。
3.2 guava cache试用场景
Guava 的 Cache跟ehcache 一样也是对内缓存,适用单节点使用。
- 你愿意消耗一些内存空间来提升速度。
- 你预料到某些键会被查询一次以上。
- 缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached这类工具)
3.3 guava cache的创建方式
- CacheLoader
- Callable callback
使用缓存前,首先问自己一个问题:有没有合理的默认方法来加载或计算与键关联的值?如果有的话,你应当使用CacheLoader。如果没有,或者你想要覆盖默认的加载运算,同时保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义,你应该在调用get时传入一个Callable实例。缓存元素也可以通过Cache.put方法直接插入,但自动加载是首选的,因为它可以更容易地推断所有缓存内容的一致性。
本篇后面例子将以cacheloader方式。
3.4 缓存数据的删除
Guava provides three basic types of eviction: size-based eviction, time-based eviction, and reference-based eviction
基于容量回收、定时回收和基于引用回收。
基于容量回收(size-based eviction):用CacheBuilder.maximumSize(long)
定时回收(Timed Eviction):CacheBuilder提供两种定时回收的方法:
- expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
- expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
主动删除:
- 单个删除 individually, using
Cache.invalidate(key)
- 批量删除 in bulk, using
Cache.invalidateAll(keys)
- 删除所有数据 to all entries, using
Cache.invalidateAll()
书上的并发场景使用:多个key并发请求的case,为了缓解后端压力,对同一个key只允许一个请求去回源获取数据,其他请求阻塞等待结果。
四 demo
需求:维护一个已开通服务的城市列表。从业务场景来说,读数据是远大于变更的,通常新开通城市会以天为单位。
所以缓存到本地就行,使用guava cache适合。
city pojo
package com.daojia.guava.cache;
import java.io.Serializable;
public class City implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
cache util
package com.daojia.guava.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class CacheUtil {
private static final String CACHE_KEY = "LocalCache";
private LoadingCache<String, Map<Integer, City>> cache =
CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Map<Integer, City>>() {
@Override
public Map<Integer, City> load(String key) throws Exception {
System.out.println("load city from service:");
List<City> allCity = new ArrayList<City>();
City citytmp = new City();
citytmp.setId(1);
citytmp.setName("beijing");
allCity.add(citytmp);
citytmp = new City();
citytmp.setId(2);
citytmp.setName("qingdao");
Map<Integer, City> cityMap = new HashMap<>();
for (City city : allCity) {
cityMap.put(city.getId(), city);
}
return cityMap;
}
});
public void refresh() {
cache.refresh(CACHE_KEY);
}
private Map<Integer, City> getCityMap() {
try {
return cache.get(CACHE_KEY);
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public Collection<City> getAllCity() {
return getCityMap().values();
}
public City getCityById(int cityId) {
return getCityMap().get(cityId);
}
public String getCityName(int cityId) {
City city = getCityById(cityId);
return null == city ? "" : city.getName();
}
}
测试类:
package com.daojia.guava.cache;
public class CacheTest {
public static void main(String[] args) {
CacheUtil cacheUtil = new CacheUtil();
for (int i = 0; i < 3; i++) {
System.out.println("vist num--- " + i + " ---");
String cname = cacheUtil.getCityName(1);
System.out.println("name:" + cname);
}
}
}
结果输出:
vist num--- 0 ---
load city from service:
name:beijing
vist num--- 1 ---
name:beijing
vist num--- 2 ---
name:beijing
可以看出第一次是模拟调用服务,后面的都是从缓存获取。