目录
com.google.common.cache.LoadingCache
本地缓存,线程安全
引入包:
- <dependency>
- <groupId>com.google.guavagroupId>
- <artifactId>guavaartifactId>
- <version>26.0-jreversion>
- dependency>
存储结构,底层实现类似于ConcurrentHashMap
class LocalCacheextends AbstractMap implements ConcurrentMap
LoadingCache定义,CacheBuilder参数
| maximumSize() | Specifies the maximum number of entries the cache may contain. 最大缓存上限 |
| expireAfterWrite() | 写过期。在put或者load的时候更新缓存的时间戳,在get过程中去判断当前时间与时间戳的差值,若大于过期时间,就会进行load操作 |
| expireAfterAccess() | 读写过期。写/读都会更新新的时间戳,所以不会很快导致缓存过期,所以当读的时候,会和最新的时间戳进行对比,最新的时间戳可能是因为写或者读而更改 |
| refreshAfterWrite() | 是指在创建缓存后,如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,默认同步去刷新缓存,如果新的缓存值还没有load到时,则会先返回旧值。 |
LoadingCache使用方法
| get(K) | 去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会抛出异常 |
| getIfPresent(key) getAllPresent(keys) | 去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会返回null,不会抛出异常。 |
| put(key, value) | 显式写入缓存,如果原来缓存里面已经存在则会覆盖原有的值 |
| invalidate(key) | 清除单个 |
| invalidateAll(keys) | 批量清除 |
| invalidateAll() | 清除所有缓存 |
| asMap() | 返回ConcurrentMap视图 |
具体代码实现
- private static final LoadingCache
numberCache = CacheBuilder.newBuilder() - .maximumSize(10)
- .expireAfterWrite(10, TimeUnit.MINUTES)
- .build(new CacheLoader
() { - @Override
- public String load(Integer key) throws Exception {
- return key + "数字测试";
- }
- });
-
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- try {
- System.out.println(numberCache.get(i));
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
- }
在定义一个本地缓存时,会去定义过期机制,分为以下
expireAfterWrite()/expireAfterAccess():表示过一定时间后缓存失效,当到达设置时间时调用get,会清理数据,然后调用load方法重新加载数据并缓存,最后返回。
refreshAfterWrite():表示一定时间后重新刷新缓存,当到达设置时间会直接返回缓存的旧值(前提是缓存为过期),同时启动一个异步线程刷新旧值。
那么一个key长时间没有被访问,该怎么清除呢?
LRU:最近最少使用
Guava Cache中是通过ConcurrentHashMap+双向链表实现的LRU算法,使用AccessQueue和RecentQueue实现LRU
AccessQueue是Guava Cache自己实现的一个比较简单的双向链表,为了性能,其设计成了非线程安全的。因此对这个Queue的操作就需要在获得了Segment的lock的场景下才能使用。
AccessQueue:负责存储对元素的读取行为记录。访问的时候将最近访问移动到链表前面。
RecencyQueue:因为AccessQueue是线程不安全的,而concurrentHashMap获取的时候并没有获取锁,所以为了确保Guava Cache的性能,它引入了RecencyQueue这个同步队列。在读取元素的时候,将所有被访问元素添加到RecencyQueue中。因为其是同步队列所以支持并发插入。这样就确保了高性能的读取能力。当在某些场景下获取到锁的时候,就再将RecencyQueue中的元素移动到AccessQueue中。
Guava Cache通过RecentQueue和AccessQueue的结合就实现了在确保get的高性能的场景下还能记录对元素的访问,从而实现LRU算法。
另外,为了支持过期机制,guava还实现了一个WriteAccess:线程不安全,负责对元素的写入行为进行记录。写入的时候将最近访问移动到链表前面。在Guava Cache中可以设置元素在写入后多久就被删除(即视为失效)。因此需要由WriteAccess来让元素根据写入时间排序(链表中每个节点页记录了元素的write时间)。
1、缓存穿透(缓存中查不到)
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。
refreshAfterWrite:只阻塞加载数据的线程,其余线程返回旧数据
如果缓存过期,恰好有多个线程读取同一个key的值,那么guava只允许一个线程去加载数据,其余线程阻塞。这虽然可以防止大量请求穿透缓存,但是效率低下。使用refreshAfterWrite可以做到:只阻塞加载数据的线程,其余线程返回旧数据。(注:如果没有旧数据,那么其余线程会阻塞)
refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。可以去实现CacheLoader.reload()完成异步刷新
2、缓存雪崩(集中失效)
数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
3、缓存击穿(一个key的请求量太大,缓存过期)
指一个key非常热点,大并发集中对这个key进行访问,当这个key在失效的瞬间,仍然持续的大并发访问就穿破缓存,转而直接请求数据库。
在缓存失效前指定让缓存刷新
guava cache提供了重新刷新与重新加载的方法,为防止缓存击穿,我们可以在缓存失效前指定让缓存刷新
定义一个本地缓存,同时设置reload与refresh机制,注:refreshAfterWrite的时间设置需要小于expireAfterWrite的时间
- private static final LoadingCache
numberCache = CacheBuilder.newBuilder() - .maximumSize(10)
- .expireAfterWrite(10, TimeUnit.MINUTES)
- .refreshAfterWrite(8, TimeUnit.SECONDS)
- .build(new CacheLoader
() { - @Override
- public String load(Integer key) throws Exception {
- return key + "数字测试";
- }
- });
本地缓存:最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;缺点:因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
分布式:最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。缺点:因为自身是一个独立的应用,本地节点都需要与其进行通信,导致依赖网络,同时如果缓存服务崩溃可能会影响所有依赖节点
1、持久化
2、缓存数据大小
3、性能