Guava Cache

什么是缓存?
想必大家第一次听到“缓存”这个概念,还是在大学的计算机专业课上。学过操作系统原理、计算机组成原理的同学都知道,在计算机系统中,存储层级可按其作用分为高速缓冲存储器(Cache)、主存储器、辅助存储器三级。当然了,这里的“高速缓存”并非本文要讨论的缓存。今天我们要讨论的缓存是软件层面的。而高速缓存是位于CPU内部的物理器件,是主存与CPU之间的一级存储器,通常由静态存储芯片(SRAM)组成,容量很小但速度比主存快得多,接近于CPU的速度。其主要作用就是缓和CPU和内存之间速度不匹配的问题,使得整机处理速度得以提升。
尽管不同于硬件层面的高速缓存,但是缓存的思想本质上是一致的,都是将数据放在离使用者最近的位置以及访问速度较快的存储介质上以加快整个系统的处理速度。软件缓存可以认为是为了缓和客户端巨大的并发量和服务端数据库(通常是关系型数据库)处理速度不匹配的问题。缓存本质上是在内存中维护的一个hash数据结构(哈希表),通常是以的形式存储。由于hash table查询时间复杂度为O(1),因此性能很高。能够有效地加速应用的读写速度,降低后端负载。

那么自行实现一个缓存需要考虑哪些基本问题呢?
1)数据结构
首先要考虑的是数据该如何存储,要选择合适的数据结构。在Java编程中,最简单的就是直接用Map集合来存储数据;复杂一点,像redis提供了多种数据结构:哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,跳跃表等数据结构;

2)更新/清除策略
缓存中的数据都是有生命周期的,要在指定时间后被删除或更新,这样才能保证缓存空间在一个可控的范围。常用的更新/清除策略有LRU(Least Recently Used最近最少使用)、FIFO( First Input First Output先进先出)、LFU(Least Frequently Used最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略。

3)线程安全
redis是单线程处理模式,就不存在线程安全问题;而本地缓存往往是可以多个线程同时访问的,所以线程安全不容忽视;线程安全问题是不应该抛给使用者去保证的,因此需要缓存自身支持。

常用的缓存技术有哪些呢?

1)分布式缓存
Memcached
Memcached 是一个开源的、高性能的、分布式的、基于内存的对象缓存系统。它能够用来存储各种格式的数据,包括字符串、图像、视频、文件等。Memcached 把数据全部存在内存之中,断电后会丢失,因此数据不能超过内存大小。且支持的数据结构较为单一,一般用于简单的key-value形式的存储。

Redis
Redis是一个开源的基于内存的数据结构存储组件,可用作数据库(nosql)、缓存和消息队列。它支持诸如字符串、散列、列表、集合、带范围查询的有序集合、位图、hyperloglogs、支持半径查询和流的地理空间索引等多种数据结构。Redis具有内置的复制、Lua脚本、LRU清除、事务和不同级别的磁盘持久化特性,并通过Redis 哨兵机制和基于Redis集群的自动分区提供高可用性。
和Memcached 相比,Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等多种数据结构的存储。Redis还支持定期把数据持久化到磁盘。

2)本地缓存
本地缓存,顾名思义就是在应用本身维护一个缓存结构,比如Java中的Map集合就比较适合做缓存。本地应用缓存最大的优点是应用本身和Cache在同一个进程内部,请求缓存非常快速,没有额外的网络I/O开销。本地缓存适合于单应用中不需要集群、各节点无需互相通信的场景。因此,其缺点是缓存跟应用程序耦合,分布式场景下多个独立部署的应用程序无法直接共享缓存,各节点都需要维护自己的单独缓存,既是对物理内存的一种浪费,也会导致数据的不一致性。Guava cache就是一种本地缓存。

话不多说,进入正题,本文主要介绍Google的缓存组件Guava Cache的实战技巧!

1.引入依赖

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.1-jre</version>
</dependency>

2.Demo1:简单使用,掌握如何创建使用Cache

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.Callable;

public class CacheService1 {
    public static void main(String[] args) throws Exception {
        Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
        // 写入/覆盖一个缓存
        cache.put("k1", "v1");
        // 获取一个缓存,如果该缓存不存在则返回一个null值
        Object value1 = cache.getIfPresent("k1");
        System.out.println("value1:" + value1);
        // 获取缓存,当缓存不存在时,则通Callable进行加载并返回,该操作是原子的
        Object getValue1 = cache.get("k1", new Callable<Object>() {
            @Override
            public Object call() throws Exception {
        //缓存加载逻辑
                return null;
            }
        });

        System.out.println("getValue1:" + getValue1);

        Object getValue2 = cache.get("k2", new Callable<Object>() {

            /**
             * 加载缓存的逻辑
             * @return
             * @throws Exception
             */
            @Override
            public Object call() throws Exception {
                return "v2";
            }
        });

        System.out.println("getValue2:" + getValue2);
    }
}

控制台输出:
value1:v1
getValue1:v1
getValue2:v2

上述程序演示了Guava Cache的读和写。Guava的缓存有许多配置选项,为了简化缓的创建,使用了Builder设计模式;Builder使用的是链式编程的思想,也就是每次调用方法后返回的是对象本身,这样可以简化配置过程。
获取缓存值时可以指定一个Callable实例动态执行缓存加载逻辑;也可以在创建Cache实例时直接使用LoadingCache。顾名思义,它能够通过CacheLoader自发的加载缓存。

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

public class CacheService2 {
    public static void main(String[] args) throws Exception {
        LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 缓存加载逻辑
                        return "value2";
                    }
                });

        loadingCache.put("k1", "value1");
        String v1 = loadingCache.get("k1");
        System.out.println(v1);

        // 以不安全的方式获取缓存,当缓存不存在时,会通过CacheLoader自动加载
        String v2 = loadingCache.getUnchecked("k2");
        System.out.println(v2);

        // 获取缓存,当缓存不存在时,会通过CacheLoader自动加载
        String v3 = loadingCache.get("k3");
        System.out.println(v3);
    }
}

控制台输出:
value1
value2
value2

3.Demo2:理解Cache的过期处理机制

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class CacheService3 {
    /**
     * 通过builder模式创建一个Cache实例
     */
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
        //设置缓存在写入5秒钟后失效
        .expireAfterWrite(5, TimeUnit.SECONDS)
        //设置缓存的最大容量(基于容量的清除)
        .maximumSize(1000)
        //开启缓存统计
        .recordStats()
        .build();

    public static void main(String[] args) throws Exception {
        //单起一个线程监视缓存状态
        new Thread() {
            public void run() {
                while (true) {
                    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                    System.out.println(sdf.format(new Date()) + " cache size: " + cache.size());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
e.printStackTrace();
                    }
                }
            }
        }.start();

        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        //写入缓存
        cache.put(1, "value1");
        //读取缓存
        System.out.println("write key:1 ,value:" + cache.getIfPresent(1));
        Thread.sleep(10000);
        // when write ,key:1 clear
        cache.put(2, "value2");
        System.out.println("write key:2 ,value:" + cache.getIfPresent(2));
        Thread.sleep(10000);
        // when read other key ,key:2 do not clear
        System.out.println(sdf.format(new Date()) + " after write, key:1 ,value:" + cache.getIfPresent(1));
        Thread.sleep(2000);
        // when read same key ,key:2 clear
        System.out.println(sdf.format(new Date()) + " final, key:2 ,value:" + cache.getIfPresent(2));
        Thread.sleep(2000);
        cache.put(1, "value1");
        cache.put(2, "value2");
        Thread.sleep(3000);
        System.out.println(sdf.format(new Date()) + " write key:1 ,value:" + cache.getIfPresent(1));
        System.out.println(sdf.format(new Date()) + " write key:2 ,value:" + cache.getIfPresent(2));
        Thread.sleep(3000);
        System.out.println(sdf.format(new Date()) + " final key:1 ,value:" + cache.getIfPresent(1));
        System.out.println(sdf.format(new Date()) + " final key:2 ,value:" + cache.getIfPresent(2));
        Thread.sleep(3000);
    }
}

控制台输出:
22:07:17 cache size: 0
write key:1 ,value:value1
22:07:18 cache size: 1
22:07:19 cache size: 1
22:07:20 cache size: 1
22:07:21 cache size: 1
22:07:22 cache size: 1
22:07:23 cache size: 1
22:07:24 cache size: 1
22:07:25 cache size: 1
22:07:26 cache size: 1
22:07:27 cache size: 1
write key:2 ,value:value2
22:07:28 cache size: 1
22:07:29 cache size: 1
22:07:30 cache size: 1
22:07:31 cache size: 1
22:07:32 cache size: 1
22:07:33 cache size: 1
22:07:34 cache size: 1
22:07:35 cache size: 1
22:07:36 cache size: 1
22:07:37 after write, key:1 ,value:null
22:07:37 cache size: 1
22:07:38 cache size: 1
22:07:39 final, key:2 ,value:null
22:07:39 cache size: 0
22:07:40 cache size: 0
22:07:41 cache size: 2
22:07:42 cache size: 2
22:07:43 cache size: 2
22:07:44 write key:1 ,value:value1
22:07:44 write key:2 ,value:value2
22:07:44 cache size: 2
22:07:45 cache size: 2
22:07:46 cache size: 2
22:07:47 final key:1 ,value:null
22:07:47 final key:2 ,value:null
22:07:47 cache size: 0
22:07:48 cache size: 0

运行上述程序,可以得出如下结论:
(1)缓存项<1,“value1”>的过期时间是5秒,但经过5秒后并没有被清除,因为还是size=1
(2)发生写操作cache.put(2, “value2”)后,缓存项<1,“value1”>被清除,因为size=1,而不是size=2
(3)发生读操作cache.getIfPresent(1)后,缓存项<2,“value2”>没有被清除,因为还是size=1,即读操作确实不一定触发清除
(4)发生读操作cache.getIfPresent(2)后,缓存项<2,“value2”>被清除,因为读的key就是2

上述机制在Guava Cache中被称为“延迟删除”,即删除总是发生得比较“晚”,并不是真正意义上的定时过期。需要依靠用户请求线程下一次读写缓存才能触发清除/更新。这也是Guava Cache的独到之处。但是这种实现方式也会存在问题:缓存会可能会存活比较长的时间,一直占用着物理内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动额外的线程,不管是实现,还是使用都比较简单(轻量)。
如果我们需要尽可能地降低延迟,可以运行自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。借助ScheduledExecutorService就可以实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。

如果使用了LoadingCache并且重写了load 方法,则缓存过期时调用get方法会自动执行load逻辑。调用getIfPresent方法获取已经过期的缓存不会执行load方法,只会执行删除策略。代码如下

static LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
        //设置缓存在写入5秒钟后失效
        .expireAfterWrite(3, TimeUnit.SECONDS)
        //设置缓存的最大容量(基于容量的清除)
        .maximumSize(1000)
        //开启缓存统计
        .recordStats()
        .build(new CacheLoader<Integer, String>() {
            @Override
            public String load(Integer key) throws Exception {
                return "new value";
            }
        });
  1. Demo3: 多线程并发获取key的场景
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.ExecutionException;

public class CacheService4 {
    public static void main(String[] args) throws Exception {
        LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 缓存加载逻辑
                        System.out.println("执行缓存加载逻辑,key:" + key);
                        return "value1";
                    }
                });

        // 获取缓存,当缓存不存在时,会通过CacheLoader自动加载
        String v1 = loadingCache.get("k");
        System.out.println("value:" + v1);

        //模拟多线程同时获取相同的key
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String vv = null;
                    try {
                        vv = loadingCache.get("kk");
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                    System.out.println("value:" + vv);
                }
            }).start();
        }

        Thread.sleep(1000);
        System.out.println("---------------------------------------");
        //模拟多线程同时获取不同的key
        for (int j = 0; j < 10; j++) {
            int i = j;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String vv = null;
                    try {
                        vv = loadingCache.get("k" + i);
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                    System.out.println("value:" + vv);
                }
            }).start();
        }
    }
}

控制台输出:
执行缓存加载逻辑,key:k
value:value1
执行缓存加载逻辑,key:kk
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1

执行缓存加载逻辑,key:k0
执行缓存加载逻辑,key:k1
value:value1
value:value1
执行缓存加载逻辑,key:k2
value:value1
执行缓存加载逻辑,key:k3
value:value1
执行缓存加载逻辑,key:k4
value:value1
执行缓存加载逻辑,key:k6
value:value1
执行缓存加载逻辑,key:k7
value:value1
执行缓存加载逻辑,key:k8
value:value1
执行缓存加载逻辑,key:k5
value:value1
执行缓存加载逻辑,key:k9
value:value1

由此可见,当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。

5.Demo4:定时刷新和定时过期的比较
上述使用方法,虽然规避了缓存击穿的情况,但是每当某个缓存值过期时,会导致大量的请求线程被阻塞。而Guava则提供了另一种缓存策略,缓存值定时刷新:只有一个更新线程调用load方法更新该缓存,其他请求线程先返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。
Demo4-1:阻塞的情况(定时过期)

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class CacheService5 {
    public static void main(String[] args) throws Exception {
        LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                //.refreshAfterWrite(2, TimeUnit.SECONDS)

                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 缓存加载逻辑
                      System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 执行缓存加载逻辑,key:" + key);
                        Thread.sleep(2000);
                        return "new value";
                    }
                });

        loadingCache.put("kk", "old value");
        System.out.println("kk:" + loadingCache.get("kk"));
        Thread.sleep(2000);

        //模拟多线程同时获取相同的key
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String vv = null;
                    try {
                        vv = loadingCache.get("kk");
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                    System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + "-value:" + vv);
                }
            }).start();
        }
    }

控制台输出:
kk:old value
22:46:00Thread-1 执行缓存加载逻辑,key:kk
22:46:02Thread-5-value:new value
22:46:02Thread-10-value:new value
22:46:02Thread-3-value:new value
22:46:02Thread-2-value:new value
22:46:02Thread-8-value:new value
22:46:02Thread-7-value:new value
22:46:02Thread-6-value:new value
22:46:02Thread-1-value:new value
22:46:02Thread-4-value:new value
22:46:02Thread-9-value:new value

由控制台输出可见所有线程阻塞直到取到新值。

Demo4-2:非阻塞的情况(定时刷新)
放开refresh这行注释,由控制台输出可见只有Thread-1获取新值,其它线程立即返回旧值
.refreshAfterWrite(2, TimeUnit.SECONDS)
//.expireAfterWrite(2, TimeUnit.SECONDS)
控制台输出:
kk:old value
22:49:47Thread-3-value:old value
22:49:47Thread-5-value:old value
22:49:47Thread-10-value:old value
22:49:47Thread-1 执行缓存加载逻辑,key:kk
22:49:47Thread-6-value:old value
22:49:47Thread-7-value:old value
22:49:47Thread-8-value:old value
22:49:47Thread-9-value:old value
22:49:47Thread-2-value:old value
22:49:47Thread-4-value:old value
22:49:49Thread-1-value:new value

另外需要注意的是,此处的定时并非真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,只有该线程调用load方法时才会校验是否过期,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

  1. Demo5:使用异步刷新
    上述方法解决的是同一个key缓存过期时多个加载相同key的线程阻塞的问题,即保证只有一个用户线程被阻塞。如果是多个不同的key呢?当缓存的key很多时,高并发环境下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞。
    解决方案:将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值(刷新完成后再访问才会是新值),这样就不会有用户线程被阻塞了。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class CacheService6 {
    public static void main(String[] args) {
        ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));

        AtomicInteger count = new AtomicInteger(0);
        LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 正常加载逻辑,key:" + key);
                        int i = count.addAndGet(1);
                        Thread.sleep(1000);//模拟耗时
                        return "value" + i;
                    }

                    @Override
                    public ListenableFuture<Object> reload(String key,
                                                           Object oldValue) throws Exception {
                        return backgroundRefreshPools.submit(new Callable<Object>() {
                            @Override
                            public Object call() throws Exception {
                                int i = count.addAndGet(1);
                                System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 异步刷新逻辑,key:" + key + ",value:" + i);
                                Thread.sleep(1000);//模拟耗时
                                return "value" + i;
                            }
                        });
                    }
                });


        try {
            System.out.println("--------------------------0轮--------------------------------------");
            for (int i = 1; i <= 10; i++) {
                cache.put("k" + i, "v" + i);
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i));
            }

            Thread.sleep(2000);
            System.out.println("--------------------------1轮--先返回第0轮的旧值----------------------------");
            for (int i = 1; i <= 10; i++) {
                System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i));
            }

            Thread.sleep(3000);
            System.out.println("--------------------------2轮--先返回第1轮的旧值----------------------------");
            for (int i = 1; i <= 10; i++) {
                System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i));
            }

            Thread.sleep(3000);
            System.out.println("--------------------------3轮--先返回第2轮的旧值----------------------------");
            for (int i = 1; i <= 10; i++) {
                System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

控制台输出如下,由此可见缓存过期后再访问都是立即返回旧值(缓存现有值),同时触发异步刷新,下一次用户现线程访问时才能获取到更新值。从下列输出还可以发现,由于线程池大小设置很小、刷新过程有时间开销,来不及刷新就被再次访问时还是输出旧值(缓存现有值)。
------------0轮------------

14:39:32main k1:v1

14:39:32main k2:v2

14:39:32main k3:v3

14:39:32main k4:v4

14:39:32main k5:v5

14:39:32main k6:v6

14:39:32main k7:v7

14:39:32main k8:v8

14:39:32main k9:v9

14:39:32main k10:v10

-------------1轮–先返回第0轮的旧值------------

14:39:34main k1:v1

14:39:34main k2:v2

14:39:34main k3:v3

14:39:34main k4:v4

14:39:34pool-1-thread-1 异步刷新逻辑,key:k1,value:1

14:39:34pool-1-thread-2 异步刷新逻辑,key:k2,value:2

14:39:34main k5:v5

14:39:34pool-1-thread-3 异步刷新逻辑,key:k3,value:3

14:39:34main k6:v6

14:39:34pool-1-thread-4 异步刷新逻辑,key:k4,value:4

14:39:34main k7:v7

14:39:34main k8:v8

14:39:34pool-1-thread-5 异步刷新逻辑,key:k5,value:5

14:39:34main k9:v9

14:39:34pool-1-thread-6 异步刷新逻辑,key:k6,value:6

14:39:34pool-1-thread-7 异步刷新逻辑,key:k7,value:7

14:39:34main k10:v10

14:39:34pool-1-thread-8 异步刷新逻辑,key:k8,value:8

14:39:34pool-1-thread-9 异步刷新逻辑,key:k9,value:9

14:39:34pool-1-thread-10 异步刷新逻辑,key:k10,value:10

------------2轮–先返回第1轮的旧值-------------
14:39:37main k1:value1

14:39:37main k2:value2

14:39:37pool-1-thread-2 异步刷新逻辑,key:k2,value:11

14:39:37main k3:value3

14:39:37pool-1-thread-1 异步刷新逻辑,key:k3,value:12

14:39:37main k4:value4

14:39:37pool-1-thread-5 异步刷新逻辑,key:k4,value:13

14:39:37main k5:value5

14:39:37main k6:value6

14:39:37main k7:value7

14:39:37pool-1-thread-3 异步刷新逻辑,key:k7,value:14

14:39:37main k8:value8

14:39:37main k9:value9

14:39:37pool-1-thread-6 异步刷新逻辑,key:k9,value:15

14:39:37main k10:value10

-----------3轮–先返回第2轮的旧值--------------

14:39:40main k1:value1

14:39:40pool-1-thread-8 异步刷新逻辑,key:k1,value:16

14:39:40main k2:value11

14:39:40pool-1-thread-4 异步刷新逻辑,key:k2,value:17

14:39:40main k3:value12

14:39:40pool-1-thread-7 异步刷新逻辑,key:k3,value:18

14:39:40main k4:value13

14:39:40pool-1-thread-9 异步刷新逻辑,key:k4,value:19

14:39:40main k5:value5

14:39:40pool-1-thread-10 异步刷新逻辑,key:k5,value:20

14:39:40main k6:value6

14:39:40main k7:value14

14:39:40pool-1-thread-2 异步刷新逻辑,key:k6,value:21

14:39:40main k8:value8

14:39:40pool-1-thread-5 异步刷新逻辑,key:k8,value:22

14:39:40main k9:value15

14:39:40pool-1-thread-1 异步刷新逻辑,key:k9,value:23

14:39:40main k10:value10

14:39:40pool-1-thread-6 异步刷新逻辑,key:k10,value:24

如果删除demo中的reload方法,则会缓存过期后访问会调用load方法,每个线程同步阻塞调用,输出如下:
--------------------------0轮---------------------------
17:45:01main k1:v1
17:45:01main k2:v2
17:45:01main k3:v3
17:45:01main k4:v4
17:45:01main k5:v5
17:45:01main k6:v6
17:45:01main k7:v7
17:45:01main k8:v8
17:45:01main k9:v9
17:45:01main k10:v10
-------------1轮–先返回第0轮的旧值------------
17:45:03main 正常加载逻辑,key:k1
17:45:03main k1:value1
17:45:04main 正常加载逻辑,key:k2
17:45:04main k2:value2
17:45:05main 正常加载逻辑,key:k3
17:45:05main k3:value3
17:45:06main 正常加载逻辑,key:k4
17:45:06main k4:value4
17:45:07main 正常加载逻辑,key:k5
17:45:07main k5:value5
17:45:08main 正常加载逻辑,key:k6
17:45:08main k6:value6
17:45:09main 正常加载逻辑,key:k7
17:45:09main k7:value7
17:45:10main 正常加载逻辑,key:k8
17:45:10main k8:value8
17:45:11main 正常加载逻辑,key:k9
17:45:11main k9:value9
17:45:12main 正常加载逻辑,key:k10
17:45:12main k10:value10
------------2轮–先返回第1轮的旧值-------------
17:45:16main 正常加载逻辑,key:k1
17:45:16main k1:value11
17:45:17main 正常加载逻辑,key:k2
17:45:17main k2:value12
17:45:18main 正常加载逻辑,key:k3
17:45:18main k3:value13
17:45:19main 正常加载逻辑,key:k4
17:45:19main k4:value14
17:45:20main 正常加载逻辑,key:k5
17:45:20main k5:value15
17:45:21main 正常加载逻辑,key:k6
17:45:21main k6:value16
17:45:22main 正常加载逻辑,key:k7
17:45:22main k7:value17
17:45:23main 正常加载逻辑,key:k8
17:45:23main k8:value18
17:45:24main 正常加载逻辑,key:k9
17:45:24main k9:value19
17:45:25main 正常加载逻辑,key:k10
17:45:25main k10:value20
-----------3轮–先返回第2轮的旧值-------------
17:45:30main 正常加载逻辑,key:k1
17:45:30main k1:value21
17:45:31main 正常加载逻辑,key:k2
17:45:31main k2:value22
17:45:32main 正常加载逻辑,key:k3
17:45:32main k3:value23
17:45:33main 正常加载逻辑,key:k4
17:45:33main k4:value24
17:45:34main 正常加载逻辑,key:k5
17:45:34main k5:value25
17:45:35main 正常加载逻辑,key:k6
17:45:35main k6:value26
17:45:36main 正常加载逻辑,key:k7
17:45:36main k7:value27
17:45:37main 正常加载逻辑,key:k8
17:45:37main k8:value28
17:45:38main 正常加载逻辑,key:k9
17:45:38main k9:value29
17:45:39main 正常加载逻辑,key:k10
17:45:39main k10:value30
因此建议使用Guava Cache时都要实现异步化的reload方法。

7.Demo6: 一个较为完整的案例

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public abstract class BaseGuavaCache<K, V> {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 缓存自动刷新周期
    protected int refreshDuration = 10;
    // 缓存刷新周期时间格式
    protected TimeUnit refreshTimeunit = TimeUnit.MINUTES;
    // 缓存过期时间(可选择)
    protected int expireDuration = -1;
    // 缓存刷新周期时间格式
    protected TimeUnit expireTimeunit = TimeUnit.HOURS;
    // 缓存最大容量
    protected int maxSize = 4;
    // 数据刷新线程池
    protected static ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));

    private LoadingCache<K, V> cache = null;

    /**
     * 用于初始化缓存值(例如某些场景下系统启动自动加载缓存)
     */
    public abstract void loadValueWhenStarted();

    /**
     * 缓存计算、加载逻辑
     *
     * @param key
     * @return
     * @throws Exception
     */
    protected abstract V getValueWhenExpired(K key) throws Exception;

    /**
     * 获取缓存
     *
     * @param key
     * @return
     * @throws Exception
     */
    public V getValue(K key) throws Exception {
        try {
            return getCache().get(key);
        } catch (Exception e) {
            logger.error("从内存缓存中获取内容时发生异常,key: " + key, e);
            throw e;
        }
    }

    public V getValueOrDefault(K key, V defaultValue) {
        try {
            return getCache().get(key);
        } catch (Exception e) {
            logger.error("从内存缓存中获取内容时发生异常,key: " + key, e);
            return defaultValue;
        }
    }

    /**
     * 设置基本属性
     */
    public BaseGuavaCache<K, V> setRefreshDuration(int refreshDuration) {
        this.refreshDuration = refreshDuration;
        return this;
    }

    public BaseGuavaCache<K, V> setRefreshTimeUnit(TimeUnit refreshTimeunit) {
        this.refreshTimeunit = refreshTimeunit;
        return this;
    }

    public BaseGuavaCache<K, V> setExpireDuration(int expireDuration) {
        this.expireDuration = expireDuration;
        return this;
    }

    public BaseGuavaCache<K, V> setExpireTimeUnit(TimeUnit expireTimeunit) {
        this.expireTimeunit = expireTimeunit;
        return this;
    }

    public BaseGuavaCache<K, V> setMaxSize(int maxSize) {
        this.maxSize = maxSize;
        return this;
    }

    public void clearAll() {
        this.getCache().invalidateAll();
    }

    /**
     * 获取cache单例
     *
     * @return
     */
    private LoadingCache<K, V> getCache() {
        if (cache == null) {
            synchronized (this) {
                if (cache == null) {
                    CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize);
                    if (refreshDuration > 0) {
                        cacheBuilder = cacheBuilder.refreshAfterWrite(refreshDuration, refreshTimeunit);
                    }
                    if (expireDuration > 0) {
                        cacheBuilder = cacheBuilder.expireAfterWrite(expireDuration, expireTimeunit);
                    }

                    cache = cacheBuilder.build(new CacheLoader<K, V>() {
                        @Override
                        public V load(K key) throws Exception {
                            return getValueWhenExpired(key);
                        }

                        @Override
                        public ListenableFuture<V> reload(final K key,
                                                          V oldValue) throws Exception {
                            return refreshPool.submit(new Callable<V>() {
                                public V call() throws Exception {
                                    return getValueWhenExpired(key);
                                }
                            });
                        }
                    });
                }
            }
        }
        return cache;
    }
}

实际业务开发时继承、实现上述抽象类,自定义业务缓存完美搞定!建议设置的刷新时间refreshDuration远小于过期时间expireDuration 。

总结一下,GuavaCache是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用Guava Cache。Guava Cache中的所有维护操作,包括清除过期缓存、写入缓存等,都是通过调用线程来驱动的,和Redis的单线程似乎有异曲同工之妙!需要起本地缓存提高响应速度,减少数据库调用频次的场景下推荐使用!

你可能感兴趣的:(java,缓存)