说明
本文主要基于面试中碰到的问题进行总结分析,可能不全。
应用场景
1. 对数据一致性要求不强,主要是由于现在web服务一般是多机房部署
2. 访问频繁,且更新不频繁的数据,前者体现了缓存的作用减少对DB的压力,后者说明了缓存毕竟是对DB数据的副本,如果经常不一致是肯定不行的
3. 缓存的数据量不宜太大,毕竟单机内存还要分配来提供服务,不能太多给你当缓存使用了
技术思想
1. 需要一个装数据的容器,一般来说就是map
2. 需要实现数据的读取和加载(将DB结果存入缓存)
3. 有一定的策略对缓存数据进行失效(否则会导致长时间数据不一致性)
4. 容器需要设置一个上限,并在达到上限时有一定的策略进行删除(见应用场景第三条)
具体实现
1. 自己实现
1-1. 申请一个ConcurrentHashMap(满了的策略不好实现)、或者LinkedHashMap(LRU好实现满了的策略,不过要自己加锁作线程安全)用作存储
1-2. 申请一个Timer()用作定时器,以实现定时对数据进行失效
2. guava localCache
2-1. 引用
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
2-2.使用
private LoadingCache<String, Book> CACHE = CacheBuilder.newBuilder()
.maximumSize(10)
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(15, TimeUnit.SECONDS)
.build(
new CacheLoader<String, Book>() {
@Override
public Book load(String name) throws Exception {
return bookDao.getByName(name);
}
}
);
2-3. 调用
public Book getByName(String name) {
return CACHE.getUnchecked(name);
}
2-4. 参数refreshAfterWrite测试
private LoadingCache<String, Book> CACHE = CacheBuilder.newBuilder()
.maximumSize(10)
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(60, TimeUnit.SECONDS)
.build(
new CacheLoader<String, Book>() {
@Override
public Book load(String name) throws Exception {
Book book = bookDao.getByName(name);
System.out.println("c:\t" + LocalTime.now().getSecond() + book);
return book;
}
}
);
public Book getByNameWithCallable(String name) {
Book result = null;
try {
result = CACHE.get(name, () -> {
Book book = bookDao.getByName(name);
System.out.println("a:\t" + LocalTime.now().getSecond() + book);
try {
Thread.sleep(5000);
} catch (Exception ex) {
}
return book;
});
System.out.println("b:\t" + LocalTime.now().getSecond() + result);
} catch (ExecutionException e) {
e.printStackTrace();
}
return result;
}
当多条线程调用时,第一条进行加载,其它的返回旧值,而且10s之后被访问就会触发刷新,即输出a那行。
第一个线程调用输出
{
"id":5,
"name":"wdmyong",
"date":{
"year":2017,
"month":"OCTOBER",
"chronology":{
"id":"ISO",
"calendarType":"iso8601"
},
"era":"CE",
"dayOfYear":290,
"dayOfWeek":"TUESDAY",
"leapYear":false,
"dayOfMonth":17,
"monthValue":10
}
}
第二条线程的输出
{
"id": 4,
"name": "wdmyong",
"date": {
"year": 2017,
"month": "OCTOBER",
"chronology": {
"id": "ISO",
"calendarType": "iso8601"
},
"era": "CE",
"dayOfYear": 290,
"dayOfWeek": "TUESDAY",
"leapYear": false,
"dayOfMonth": 17,
"monthValue": 10
}
}
我的id用了自增,可以看出在第一条线程计算的时候返回的是原来的id为4,等自己计算完之后,第一条线程返回的是id为5的数据。
2-4. 上述过程应该还可以完全异步,即第一条计算线程也返回原值,让它异步计算自己计算去,后续待研究。
3.