随着互联网的发展, 数据量不断增长, 用户对性能要求的不断提升, 在开发项目中使用缓存已经是必不可少的一种手段了我们会将一些很少或者较少变化, 对及时性要求不高的数据存放在缓存中, 以减少数据库的压力和负载常用的缓存分为堆内缓存, 堆外缓存, 以及分布式缓存而谈到堆内缓存, 比较常用的就是 Guava 里提供的 LoadingCache
本文中会从源码角度来分析 LoadingCache 中数据是如何被加载到缓存中, 如何在多线程的场景下保证只有一个线程会去加载缓存数据这一点在实际项目中是至关重要的, 试想一下如果有 100 个线程同时到达, 并且同时去数据库里读取数据并加载到缓存, 很有可能就会发生缓存击穿, 造成数据库负载急剧上升, 更有甚者可能会直接搞挂数据库
直接来一段 Code Snippet
- LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
- .maximumSize(1000)
- .expireAfterWrite(10, TimeUnit.MINUTES)
- .removalListener(MY_LISTENER)
- .build(
- new CacheLoader<Key, Graph>() {
- public Graph load(Key key) throws AnyException {
- return createExpensiveGraph(key);
- }
- });
上面这段代码是最常见的使用 LoadingCache 的方式
创建了一个缓存实例
指定最多可以存 1000 个缓存项
在加载缓存后 10 分钟过期
注册了一个自定义的 CacheLoader 来告诉 LoadingCache 如何在缓存失效后加载缓存
但是这段代码有一个非常致命的问题就是当缓存不存在或者过期的情况下, LoadingCache 只会允许一个请求去加载缓存, 其他并发请求会阻塞在那边直至缓存加载完毕, 那如果加载缓存过程比较慢的话, 就会造成并发请求被阻塞, 影响服务的整体性能, 下面的这段代码模拟了这种情况
模拟并发请求的代码
控制台输出
那 LoadingCache 究竟是如何实现上文所说的这种机制的呢? 让我们一起顺着 cache.get("anyKey") 语句来探索下源码
LocalCache.get
可以看到在这个方法里调用了 localCache.getOrLoad(key) 方法, localCache 的类型是 LocalCache, 它继承了 AbstractMap 并且实现了 ConcurrentMap 接口看到这里大家可能会问不是很多地方都说实现一个缓存只要使用 LinkedHashMap 就可以了吗? 为什么还要重新去实现一遍呢? 其实这里的关键在于 LinkedHashMap 并不是线程安全的, 即使你在外部调用的地方去加上锁, 那锁也是非常粗粒度的, 然而 LocalCache 借鉴了 ConcurrentHashMap 的实现方式在内部使用了分段锁, 大大提高了性能这一块源码在这里就不展开了, 如果大家有兴趣的话可以自己去探究下
在继续往里跟, 可以看到调用了一个重载的 get 方法, 还传了一个 defaultLoader, 这个 defaultLoader 就是我们在初始化 LoadingCache 时定义的那个 CacheLoader, 告诉 LoadingCache 如何去加载缓存
LocalCache.get
这个重载的 get 方法里首先先计算出缓存 key 的 hash code, 然后找到 hash code 所在的 Segment, 再从 Segment 里尝试获取缓存值
LocalCache.get
继续来看 segmentFor(hash).get(key,hash,loader) 中的 get 方法
Segment.get
现在来看一下上文提到的 waitForLoadingValue 方法, 这个方法会调用 Future 的阻塞 get 方法去获取缓存值, 如果另外一个线程在加载缓存, 当前线程会阻塞直到缓存加载完毕, 如下图
Uninterruptibles.getUninterruptibly
再看一下 lockedGetOrLoad 方法, 这个方法中主要做的就是加锁, 然后看缓存项是否存在, 或者是否已经被其他线程加载, 如果没有的话, 就尝试用一开始用户传进来的 loader 加载缓存
LocalCache.lockedGetOrLoad
所以从 LoadingCache 的源码中可以看到, LoadingCache 是会保证同一时间只有一个线程去加载缓存, 从而防止缓存击穿的问题发生, 但是如果缓存加载需要比较长的时间的话, 会导致其他的线程都阻塞在那边, 影响系统的可用性, 在下篇文章中会介绍如何利用 LoadingCache 的 refresh 机制来解决这个问题
来源: http://www.jianshu.com/p/daee10efb566