告别手动加载!LoadingCache 让缓存操作更智能

1,148 阅读5分钟

引言

凡是用过缓存的相信都有一个痛点,那就是在查询缓存的时候都需要写大量的判断,当缓存不存在的时候我们是需要去数据库进行查询的,然后查询的结果还要再来丢进缓存里面,烦不胜烦,要是手误一下,还可能代码没闭环,给自己挖一个坑。

一、一般情况缓存的查询使用方法

在日常开发中,我们通常会先检查缓存中是否存在目标数据,如果不存在,再从数据库或其他数据源加载数据并存入缓存,以供后续使用。

时序图如下:

sequenceDiagram
    participant User
    participant Service
    participant Cache
    participant Database
    
    Note over User,Database: 通过缓存查询用户数据

    User->>+Service: 查询数据
    Service->>+Cache: 查询数据
    Cache-->>-Service: 返回查询结果
    alt 命中缓存
        Service-->>-User: 返回查询结果给调用者
    end
    alt 未命中缓存
        Service->>+Database: 查询数据
        Database-->>-Service: 返回数据
        Service->>+Cache: 将数据写入缓存
        Cache-->>-Service: 数据写入成功
        Service-->>+User: 返回查询结果给调用者
    end    

实现代码如下:

public User getUser(String id) {
    User user = RedisUtils.getInstance().get(id, User.class);
    if (Objects.isNull(user)) {
        user = mapper.selectByPrimaryKey(id);
        if (Objects.nonNull(user)) {
            RedisUtils.getInstance().put(id, user);
        }
     }
    return user;
}

类似上面这种,每一个查询的地方,我们都要做对应的判断,实际可以看到代码还是比较麻烦的。这种方式冗余且容易出错。

而且上面还只是查询的的代码,实际开发中,我们还要保证缓存的一致性,需要自己去写刷新缓存的代码。

小结:传统缓存管理存在两个痛点:手动加载相当繁琐缓存过期与刷新相当复杂

二、认识LoadingCache

LoadingCache 是 Google Guava 提供的一个强大的缓存接口,它继承自Cache接口,能够自动加载和管理缓存数据。它通过 CacheLoader 来指定加载逻辑,当缓存中不存在请求的键时,会自动调用 CacheLoaderload 方法加载数据。

image.png

2.1 LoadingCache的特性

  • 自动加载:当请求的键不存在于缓存中时,LoadingCache 会自动调用 CacheLoaderload 方法来加载数据,并将其存储到缓存中。

  • 缓存过期策略:支持多种过期策略,例如基于时间的过期(expireAfterAccess访问时间、expireAfterWrite写入时间)和基于大小的过期(maximumSize)。

  • 自动刷新:通过 refreshAfterWrite 方法,可以设置缓存项在写入后经过一定时间自动刷新。

  • 并发支持:支持高并发访问,可通过 concurrencyLevel 参数设置并发级别。

  • 缓存统计:通过 recordStats 方法,可以开启缓存统计功能,获取命中率、加载时间等统计信息。

  • 监听器:支持 RemovalListener,可以在缓存项被移除时执行特定操作。

2.2 LoadingCache的创建和使用

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

import java.util.concurrent.TimeUnit;

public class Demo {
    public static void main(String[] args) throws Exception {
        // 创建 LoadingCache
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(1024 * 1024) // 设置最大缓存项数
                .expireAfterWrite(8, TimeUnit.HOURS) // 设置写入后 8 小时过期
                .refreshAfterWrite(90, TimeUnit.MINUTES) // 设置写入后 90 分钟刷新
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        return fetchDataFromDatabase(key); // 模拟从数据库加载数据
                    }
                });

        // 使用缓存
        System.out.println(cache.get("key1")); // 第一次加载
    }

    private static String fetchDataFromDatabase(String key) {
        // 模拟数据库操作
        return "Data for " + key;
    }
}

在上面的案例中

  • CacheLoaderload 方法定义了如何加载数据。

  • maximumSize 设置了缓存的最大项数。

  • expireAfterWrite 设置了缓存项的过期时间。

  • refreshAfterWrite 设置了缓存项的刷新时间。

三、 LoadingCache 的智能化特性

简单认识了,回归正文,文章标题就说了 LoadingCache 让缓存操作更智能,那么到底怎么智能呢?下面我们一起来看看LoadingCache有那些智能化特性。

3.1 自动加载缓存数据

LoadingCache 通过 CacheLoader 自动加载缓存数据,无需手动检查缓存是否存在。减少代码量,降低出错风险,提升开发效率。

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            // 自动加载逻辑...注意,这里直接就是从数据源里面去加载数据,不需要做空判断
            return fetchDataFromDatabase(key); 
        }
    });

3.2 自动刷新缓存

LoadingCache 通过 refreshAfterWrite 方法,LoadingCache 可以在缓存项写入后经过一定时间自动刷新。确保缓存数据的时效性,无需手动更新缓存。

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
    .refreshAfterWrite(5, TimeUnit.MINUTES) // 5分钟后自动刷新...这个只是案例,实际应用要根据业务场景自己定义刷新时间
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            return fetchDataFromDatabase(key);
        }
    });

3.3 异常处理与容错机制

LoadingCache 在加载数据时可能会抛出异常,但可以通过 RemovalListener 或其他机制进行容错处理。提高系统的健壮性,避免因加载失败导致的系统崩溃。

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
    .removalListener(new RemovalListener<String, String>() {
        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            if (notification.getCause() == RemovalCause.EXPIRED) {
                System.out.println("Cache entry expired: " + notification.getKey());
            }
        }
    })
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            return fetchDataFromDatabase(key);
        }
    });

注意:我看到有部分文章将统计与监控写成了LoadingCache的智能特性,不能说错,但是实际这些是继承自他的父类的,不是他自己私有的特性。

四、总结

LoadingCache 的核心优势主要在于自动加载、自动刷新、异常处理这些,其在简化开发和程序健壮方面的作用非常明显。

可以说它非常优雅的解决了传统缓存管理存在两个痛点:手动加载相当繁琐缓存过期与刷新相当复杂

注意:本文并没有写缓存的应用场景,这个能找到这篇文章的应该都非常熟悉了,就不做额外介绍。

另外要了解他的父类,可以阅读我的另外一篇文章# 好用的Java内存缓存库,Guava Cache

最后:如果你从本篇文章中学到一点点东西,麻烦发财的小手点一下赞,如有疑问或者错误,欢迎提出和指正。