在实际的开发过程中,我们经常需要用到缓存。使用缓存常见的一个场景就是key不在缓存中,这个时候我们会去读取这个key对应的值,然后把这个值放到缓存中,代码如下:
public class CacheNoFuture { private ConcurrentHashMapcache = new ConcurrentHashMap<>(); public Object get(String key) { Object o = cache.get(key); if (o == null) { o = readFromDB(key); cache.put(key, o); } return o; } private Object readFromDB(String key) { return new Object(); }复制代码
这个代码有一个比较大的问题就是:如果同一时刻大量的请求发现o是空,都会去调用readFromDB,导致缓存被击穿了,可能的后果就是数据库直接被冲垮。理想的情况是同一个key同一时间只有一个thread去调用readFromDB,其他的thread等待它的结果。我们看一下Cglib包下面的LoadingCache是怎么做的。
1.代码位置
目前我使用的cglib的maven配置如下:
复制代码 cglib cglib 3.2.5
LoadingCache代码在net.sf.cglib.core.internal包下面。
2.代码分析
先看一下它的get方法,还是比较好理解的,有点不一样的是它判断了从map里面取到的内容是不是FutureTask,这个会在后面介绍。接下来我们看一下当从缓存里面读到的数据为空或者为FutureTask的时候,它做了什么。
public V get(K key) { final KK cacheKey = keyMapper.apply(key); Object v = map.get(cacheKey); if (v != null && !(v instanceof FutureTask)) { return (V) v; } return createEntry(key, cacheKey, v);}复制代码
这段代码还是比较好理解的,我觉得理一下我标注的5行基本就差不多了。
- line 1: 从 get 我们知道进入createEntry的条件是v不为空并且v不是FutureTask ,这一行判断v不为空,那只能说明v是FutureTask,所以把v赋值给task,表示目前已经有一个线程在加载数据了。
- line 2: 很多线程正在竞争的去加载数据,但是只有putIfAbsent返回为空的那个成为creator
- line 3、4: 结合line 2的解释,没有竞争成功的,会获得creator的FutureTask(加载没有完成)或者V(加载已经完成)
- line 5:加载完成之后放回到cache里面,这也就是为什么有line 4的原因了。
protected V createEntry(final K key, KK cacheKey, Object v) { FutureTasktask; boolean creator = false; if (v != null) { //line 1 // Another thread is already loading an instance task = (FutureTask ) v; } else { task = new FutureTask (new Callable () { public V call() throws Exception { return loader.apply(key); } }); Object prevTask = map.putIfAbsent(cacheKey, task); if (prevTask == null) { //line 2 // creator does the load creator = true; task.run(); } else if (prevTask instanceof FutureTask) { //line 3 task = (FutureTask ) prevTask; } else { //line 4 return (V) prevTask; } } V result; try { result = task.get(); } catch (InterruptedException e) { throw new IllegalStateException("Interrupted while loading cache item", e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException) { throw ((RuntimeException) cause); } throw new IllegalStateException("Unable to load cache item", cause); } if (creator) { //line 5 map.put(cacheKey, result); } return result; }复制代码
3.总结
这种做法也有明显的不足:
- 加载成功之后,key对应的值就不会再变了,即使我们数据源头发生了变化。