Guava LoadingCache本地缓存的正确使用姿势——异步加载

guava,loadingcache,本地,缓存,正确,使用,姿势,异步,加载 · 浏览次数 : 1685

小编点评

## 3.2.2 验证refreshAfterWrite加线程池 这段代码测试了使用`refreshAfterWrite`方法进行异步缓存刷新,并通过线程池来实现并发操作。 **测试步骤:** 1. 创建一个`LoadingCache`实例,并设置并发级别为3,以及刷新时间为5秒。 2. 设置初始容量和最大容量分别为1000和2000。 3. 定义一个`load`方法,用于在缓存中获取数据。 4. 在线程池中提交一个任务,使用`load`方法来获取数据。 5. 再次获取数据并打印时间,以验证缓存刷新效果。 6. 在指定时间(6000秒)后,再次获取数据并打印时间,以验证缓存失效的效果。 7. 最后,测试每次获取数据的结果,确保它与最初获取数据的结果相同。 **测试结果:** 测试结果显示,第二次获取数据时等待时间更长,因为它需要等待缓存刷新。 **验证结果:** 通过测试,验证了`refreshAfterWrite`加线程池的方法能实现异步缓存刷新,并能避免缓存失效的问题。 **注意:** * `refreshAfterWrite`方法会延迟指定的刷新时间,直到数据有效。 * 即使设置了刷新时间,但如果数据缓存中没有数据,也会延迟刷新。 * `refreshAfterWrite`方法会阻塞用户的线程,因此需要在使用之前考虑性能影响。 **总结:** `refreshAfterWrite`加线程池是一个可行的方法,可以实现异步缓存刷新,并避免缓存失效的问题。但是,使用`refreshAfterWrite`方法时,需要注意刷新时间设置和线程池的配置。

正文

1. 【背景】AB实验SDK耗时过高

同事在使用我写的实验平台sdk之后,吐槽耗时太高,获取实验数据分流耗时达到700ms,严重影响了主业务流程的执行

2. 【分析】缓存为何不管用

我记得之前在sdk端加了本地缓存(使用了LoadingCache),不应该这样慢

通过分析,只有在缓存失效之后的那一次请求耗时会比较高,又因为随着实验数据的增加,获取实验确实会花费这么多时间

那如何解决呢?如果不解决,每次缓存失效,至少会有一个请求阻塞获取实验数据导致超时

3. 【工具】Guava LoadingCache

Guava是一个谷歌开源Java工具库,提供了一些非常实用的工具。LoadingCache就是其中一个,是一个本地缓存工具,支持配置加载函数,定时失效

基本用法:

  1. 其中的CacheLoader是当key对应value不存在时,会使用重载的load方法取并放入cache
  2. cache.get从缓存获取数据
LoadingCache<Long, String> cache
                // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为3,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(3)
                // 过期
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 初始容量
                .initialCapacity(1000)
                // 最大容量,超过LRU
                .maximumSize(2000).build(new CacheLoader<Long, String>() {

                    @Override
                    @Nonnull
                    public String load(@Nonnull Long key) throws Exception {
                        Thread.sleep(1000);
                        return DATE_FORMATER.format(Instant.now());
                    }
                });
// 获取数据
cache.get(1L)

3.1 LoadingCache的失效和刷新

既然用到缓存,避免不了的问题就是如何更新缓存中的值,使其不能太旧,又能兼顾性能

LoadingCache常用两个方法来实现失效:

  1. expireAfterWrite(long, TimeUnit)
  2. refreshAfterWrite(long, TimeUnit)

官方文档给出的区别

Refreshing is not quite the same as eviction. As specified in LoadingCache.refresh(K), refreshing a key loads a new value for the key, possibly asynchronously. The old value (if any) is still returned while the key is being refreshed, in contrast to eviction, which forces retrievals to wait until the value is loaded anew

  • refresh期间会返回旧值
  • expire会等待load方法的新值

我们的场景就是某个请求会阻塞等待数据返回,所以如果我们用refresh方法过期的话,就能使耗时变低,带来的问题是当时获取的数据是旧的,对于当前这个场景是可以接受的

3.2 refreshAfterWrite如何异步加载

3.2.1 验证expireAfterWrite

public static void testExpireAfterWrite() throws ExecutionException, InterruptedException {
        LoadingCache<Long, String> cache
                // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为3,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(3)
                // 过期
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 初始容量
                .initialCapacity(1000)
                // 最大容量,超过LRU
                .maximumSize(2000).build(new CacheLoader<Long, String>() {

                    @Override
                    @Nonnull
                    public String load(@Nonnull Long key) throws Exception {
                        Thread.sleep(1000);
                        return DATE_FORMATER.format(Instant.now());
                    }
                });
        log.info("cache get");
        String rs = cache.get(10L);
        log.info("cache rs:{}", rs);

        Thread.sleep(6000);

        log.info("cache get");
        rs = cache.get(10L);
        log.info("cache rs:{}", rs);
    }

输出结果,从打印的时间可以看出,第二次get同步等待结果

     15:33:44.160 [main] INFO cache.LoadingCacheTest - cache get
     15:33:45.192 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:33:45
     15:33:51.199 [main] INFO cache.LoadingCacheTest - cache get
     15:33:52.225 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:33:52

3.2.2 验证refreshAfterWrite

public static void testRefreshAfterWrite() throws ExecutionException, InterruptedException {
        LoadingCache<Long, String> cache
                // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为3,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(3)
                // 过期
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                // 初始容量
                .initialCapacity(1000)
                // 最大容量,超过LRU
                .maximumSize(2000).build(new CacheLoader<Long, String>() {

                    @Override
                    @Nonnull
                    public String load(@Nonnull Long key) throws Exception {
                        Thread.sleep(1000);
                        return DATE_FORMATER.format(Instant.now());
                    }
                });
        log.info("cache get");
        String rs = cache.get(10L);
        log.info("cache rs:{}", rs);

        Thread.sleep(6000);

        log.info("cache get");
        rs = cache.get(10L);
        log.info("cache rs:{}", rs);
    }

输出结果,从打印的时间可以看出,第二次也get同步等待结果

     15:35:31.064 [main] INFO cache.LoadingCacheTest - cache get
     15:35:32.090 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:35:32
     15:35:38.099 [main] INFO cache.LoadingCacheTest - cache get
     15:35:39.147 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:35:39

3.2.3 验证refreshAfterWrite加线程池

public static void testRefreshAfterWriteWithReload() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        LoadingCache<Long, String> cache
                // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为3,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(3)
                // 过期
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                // 初始容量
                .initialCapacity(1000)
                // 最大容量,超过LRU
                .maximumSize(2000).build(new CacheLoader<Long, String>() {

                    @Override
                    @Nonnull
                    public String load(@Nonnull Long key) throws Exception {
                        Thread.sleep(1000);
                        return DATE_FORMATER.format(Instant.now());
                    }

                    @Override
                    @Nonnull
                    public ListenableFuture<String> reload(@Nonnull Long key, @Nonnull String oldValue) throws Exception {
                        ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
                            Thread.sleep(1000);
                            return DATE_FORMATER.format(Instant.now());
                        });
                        executorService.submit(futureTask);
                        return futureTask;
                    }
                });
        log.info("cache get");
        String rs = cache.get(10L);
        log.info("cache rs:{}", rs);

        Thread.sleep(6000);

        log.info("cache get");
        rs = cache.get(10L);
        log.info("cache rs:{}", rs);

        Thread.sleep(3000);

        log.info("cache get");
        rs = cache.get(10L);
        log.info("cache rs:{}", rs);
    }

输出结果,从打印的时间可以看出,第二次不同步等待结果,获取旧值,第三次获取了第二次提交的异步任务的值

     15:41:45.194 [main] INFO cache.LoadingCacheTest - cache get
     15:41:46.224 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:46
     15:41:52.230 [main] INFO cache.LoadingCacheTest - cache get
     15:41:52.279 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:46
     15:41:55.284 [main] INFO cache.LoadingCacheTest - cache get
     15:41:55.284 [main] INFO cache.LoadingCacheTest - cache rs:2022-11-08 15:41:53

3.2.4 更加优雅的写法

如果觉的上面的写法比较啰嗦,可以这样写,效果一样

        CacheLoader<Long, String> cacheLoader = CacheLoader.asyncReloading(new CacheLoader<Long, String>() {

            @Override
            @Nonnull
            public String load(@Nonnull Long key) throws Exception {
                Thread.sleep(1000);
                return DATE_FORMATER.format(Instant.now());
            }
        }, executorService);
        LoadingCache<Long, String> cache
                // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为3,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(3)
                // 过期
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                // 初始容量
                .initialCapacity(1000)
                // 最大容量,超过LRU
                .maximumSize(2000).build(cacheLoader);

refreshAfterWrite的缺点:到了指定时间不过期,而是延迟到下一次查询,所以数据有可能过期了很久(假如这一段时间一直没有查询)

所以可以使用efreshAfterWrite和expireAfterWrite配合使用:

比如说控制缓存每1s进行refresh,如果超过2s没有访问,那么则让缓存失效,下次访问时不会得到旧值,而是必须得待新值加载

4. 【总结】异步加载缓存是可行的

最终我们使用了LoadingCache的refreshAfterWrite加线程池的方法实现了异步加载缓存数据,并且没有阻塞用户的线程

  • 这种方法类似CopyOnWrite,在写操作的同时复制一份,读的时候先使用旧值

不过这种做法也有缺点,会导致缓存数据不是最新的,最新数据会延迟到下次查询之后的查询,需要根据场景综合考虑

参考

[1] Github Guava Doc
[2] 深入理解guava-cache的refresh和expire刷新机制

与Guava LoadingCache本地缓存的正确使用姿势——异步加载相似的内容:

Guava LoadingCache本地缓存的正确使用姿势——异步加载

1. 【背景】AB实验SDK耗时过高 同事在使用我写的实验平台sdk之后,吐槽耗时太高,获取实验数据分流耗时达到700ms,严重影响了主业务流程的执行 2. 【分析】缓存为何不管用 我记得之前在sdk端加了本地缓存(使用了LoadingCache),不应该这样慢 通过分析,只有在缓存失效之后的那一次

GuavaCache、EVCache、Tair、Aerospike 缓存框架比较

Guava Cache、EVCache、Tair、Aerospike 是不同类型的缓存解决方案,它们各有特点和应用场景。下面我会逐一分析这些缓存系统的优势、应用场景,并提供一些基本的代码示例。

Springboot+Guava实现单机令牌桶限流

# 令牌桶算法 > 系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。 >

浅析本地缓存技术-Guava Cache

本文简要叙述了guava cache的应用场景以及简单的使用方式,通过源码对于guava cache的存储原理以及简单的读写方法进行了介绍。相信通过阅读本文,能够对于常见的guava cache有一个大致的认知。

京东云开发者|深入JDK中的Optional

Optional最早是Google公司Guava中的概念,代表的是可选值。Optional类从Java8版本开始加入豪华套餐,主要为了解决程序中的NPE问题,从而使得更少的显式判空,防止代码污染,另一方面,也使得领域模型中所隐藏的知识,得以显式体现在代码中。Optional类位于java.util包下,对链式编程风格有一定的支持。实际上,Optional更像是一个容器,其中存放的成员变量是一个T类