本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!


解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

大家好,又见面了。

上一篇文章中,咱们继Guava Cache之后,又知道了青出于蓝的Caffeine。作为一种对外供给黑盒缓存才能的专门组件,Caffeine根据穿透型缓存模式进行构建。也即对外供给数据查询接口,会优先在缓存中进行查询,若射中缓存则回来成果,未射中则测验去实在的源端(如:数据库)去获取数据并回填到缓存中,回来给调用方。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

与Guava Cache类似,Caffeine回源填充主要有两种手法:

  • Callable办法

  • CacheLoader办法

依据履行调用办法不同,又能够细分为同步堵塞办法与异步非堵塞办法。

本文咱们就一同探寻下Caffeine的多种不同的数据回源办法,以及对应的实践运用。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

同步办法

同步办法是最常被运用的一种形式。查询缓存、数据回源、数据回填缓存、回来履行成果等一系列操作都是在一个调用线程同步堵塞完结的。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

Callable

在每次get恳求的时分,传入一个Callable函数式接口具体完结,当没有射中缓存的时分,Caffeine框架会履行给定的Callable完结逻辑,去获取实在的数据而且回填到缓存中,然后回来给调用方。

public static void main(String[] args) {
    Cache<String, User> cache = Caffeine.newBuilder().build();
    User user = cache.get("123", s -> userDao.getUser(s));
    System.out.println(user);
}

Callable办法的回源填充,有个明显的优势就是调用方能够依据自己的场景,灵活的给定不同的回源履行逻辑。可是这样也会带来一个问题,就是假如需求获取缓存的当地太多,会导致每个调用的当地都得指定下对应Callable回源办法,调用起来比较麻烦,且关于需求确保回源逻辑一致的场景管控才能不够强势,无法约束一切的调用方运用相同的回源逻辑。

这种时分,便需求CacheLoader上台了。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

CacheLoader

在创立缓存方针的时分,能够通在build()办法中传入指定的CacheLoader方针的办法来指定回源时默许运用的回源数据加载器,这样当运用方调用get办法获取不到数据的时分,框架就会自动运用给定的CacheLoader方针履行对应的数据加载逻辑。

比方下面的代码中,便在创立缓存方针时指定了当缓存未射中时经过userDao.getUser()办法去DB中履行数据查询操作:

public LoadingCache<String, User> createUserCache() {
    return Caffeine.newBuilder()
            .maximumSize(10000L)
            .build(key -> userDao.getUser(key));
}

比较于Callable办法,CacheLoader更适用一切回源场景运用的回源战略都固定且一致的状况。对具体事务运用的时分愈加的友好,调用get办法也愈加简单,只需求传入带查询的key值即可。

上面的示例代码中还有个需求重视的点,即创立缓存方针的时分指定了CacheLoader,终究创立出来的缓存方针是LoadingCache类型,这个类型是Cache的一个子类,扩展供给了无需传入Callable参数的get办法。进一步地,咱们打印出对应的具体类名,会发现得到的缓存方针具体类型为:

com.github.benmanes.caffeine.cache.BoundedLocalCache.BoundedLocalLoadingCache

当然,假如创立缓存方针的时分没有指定最大容量限制,则创立出来的缓存方针还可能会是下面这个:

com.github.benmanes.caffeine.cache.UnboundedLocalCache.UnboundedLocalManualCache

经过UML图,能够明晰的看出其与Cache之间的承继与完结链路状况:

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

由于LoadingCache是Cache方针的子类,依据JAVA中类承继的特性,LoadingCache也彻底具有Cache一切的接口才能。所以,关于大部分场景都需求固定且一致的回源办法,可是某些特殊场景需求自定义回源逻辑的状况,也能够经过组合运用Callable的办法来完结。

比方下面这段代码:

public static void main(String[] args) {
    LoadingCache<String, User> cache = Caffeine.newBuilder().build(userId -> userDao.getUser(userId));
    // 运用CacheLoader回源
    User user = cache.get("123");
    System.out.println(user);
    // 运用自定义Callable回源
    User techUser = cache.get("J234", userId -> {
        // 仅J最初的用户ID才会去回源
        if (!StringUtils.isEmpty(userId) && userId.startsWith("J")) {
            return userDao.getUser(userId);
        } else {
            return null;
        }
    });
    System.out.println(techUser);
}

上述代码中,构造的是一个指定了CacheLoader的LoadingCache缓存类型,这样关于群众场景能够直接运用get办法由CacheLoader供给一致的回源才能,而特殊场景中也能够在get办法中传入需求的定制化回源Callable逻辑。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

不回源

在实践的缓存应用场景中,并非是一切的场景都要求缓存没有射中的时分要去履行回源查询。关于一些事务规划上无需履行回源操作的恳求,也能够要求Caffeine不要履行回源操作(比方黑名单列表,只需用户在黑名单就制止操作,不在黑名单则答应持续往后操作,由于大部分恳求都不会射中到黑名单中,所以不需求履行回源操作)。为了完结这一点,在查询操作的时分,能够运用Caffeine供给的免回源查询办法来完结。

具体梳理如下:

接口 功用阐明
getIfPresent 从内存中查询,假如存在则回来对应值,不存在则回来null
getAllPresent 批量从内存中查询,假如存在则回来存在的键值对,不存在的key则不出现在成果集里

代码运用演示如下:

public static void main(String[] args) {
    LoadingCache<String, User> cache = Caffeine.newBuilder().build(userId -> userDao.getUser(userId));
    cache.put("124", new User("124", "张三"));
    User userInfo = cache.getIfPresent("123");
    System.out.println(userInfo);
    Map<String, User> presentUsers =
            cache.getAllPresent(Stream.of("123", "124", "125").collect(Collectors.toList()));
    System.out.println(presentUsers);
}

履行成果如下,能够发现履行的过程中并没有触发自动回源与回填操作:

null
{124=User(userName=张三, userId=124)}

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

异步办法

CompletableFuture并行流水线才能,是JAVA8异步编程领域的一个重大改进。能够将一系列耗时且无依靠的操作改为并行同步处理,并等候各自处理成果完结后持续进行后续环节的处理,由此来下降堵塞等候时刻,然后达到下降恳求链路时长的作用。

许多小伙伴对JAVA8之后的CompletableFuture并行处理才能接触的不是许多,有爱好的能够移步看下我之前专门介绍JAVA8流水线并行处理才能的介绍《JAVA根据CompletableFuture的流水线并行处理深度实践,满满干货》,相信能够让你对ComparableFututre并行编程有全面的知道与理解。

Caffeine完美的支撑了在异步场景下的流水线处理运用场景,回源操作也支撑异步的办法来完结。

异步Callable

要想支撑异步场景下运用缓存,则创立的时分必须要创立一个异步缓存类型,能够经过buildAsync()办法来构建一个AsyncCache类型缓存方针,然后能够在异步场景下进行运用。

看下面这段代码:

public static void main(String[] args) {
    AsyncCache<String, User> asyncCache = Caffeine.newBuilder().buildAsyn();
    CompletableFuture<User> userCompletableFuture = asyncCache.get("123", s -> userDao.getUser(s));
    System.out.println(userCompletableFuture.join());
}

上述代码中,get办法传入了Callable回源逻辑,然后会开端异步的加载处理操作,并回来了个CompletableFuture类型成果,终究假如需求获取其实践成果的时分,需求等候其异步履行完结然后获取到终究成果(经过上述代码中的join()办法等候并获取成果)。

咱们能够比对下同步异步两种办法下Callable逻辑履行线程状况。看下面的代码:

public static void main(String[] args) {
    System.out.println("main thread:" + Thread.currentThread().getId());
    // 同步办法
    Cache<String, User> cache = Caffeine.newBuilder().build();
    cache.get("123", s -> {
        System.out.println("同步callable thread:" + Thread.currentThread().getId());
        return userDao.getUser(s);
    });
    // 异步办法
    AsyncCache<String, User> asyncCache = Caffeine.newBuilder().buildAsync();
    asyncCache.get("123", s -> {
        System.out.println("异步callable thread:" + Thread.currentThread().getId());
        return userDao.getUser(s);
    });
}

履行成果如下:

main thread:1
同步callable thread:1
异步callable thread:15

成果很明显的能够看出,同步处理逻辑中,回源操作直接占用的调用线程进行操作,而异步处理时则是独自线程负责回源处理、不会堵塞调用线程的履行 —— 这也是异步处理的优势地点。

看到这儿,也许会有小伙伴有疑问,虽然是异步履行的回源操作,可是终究仍是要在调用线程里边堵塞等候异步履行成果的完结,似乎没有看出异步有啥优势?

异步处理的魅力,在于当一个耗时操作履行的同时,主线程能够持续去处理其它的事情,然后其他事务处理完结后,直接去取异步履行的成果然后持续往后处理。假如主线程无需履行其他处理逻辑,彻底是堵塞等候异步线程加载完结,这种状况确实没有必要运用异步处理。

幻想一个生活中的场景:

周末歇息的你出去逛街,去咖啡店点了一杯咖啡,然后服务员会给你一个订单小票。 当服务员在后台制造咖啡的时分,你并没有在店里等候,而是出门到近邻甜品店又买了个面包。 当面包买好之后,你回到咖啡店,拿着订单小票去取咖啡。 取到咖啡后,你边喝咖啡边把面包吃了……嗝~

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

这种状况应该比较好理解了吧?假如是同步处理,你买咖啡的时分,需求在咖啡店一向等到咖啡做好然后才能再去甜品店买面包,这样耗时就比较长了。而选用异步处理的战略,你在等候咖啡制造的时分,持续去甜品店将面包买了,然后回来等候咖啡完结,这样全体的时刻就缩短了。当然,假如你只想买个咖啡,也不需求买甜品面包,即你等候咖啡制造期间没有其他事情需求处理,那这时分你在不在咖啡店一向等到咖啡完结,都没有差异。

回到代码层面,下面代码演示了异步场景下AsyncCache的运用。

public boolean isDevUser(String userId) {
    // 获取用户信息
    CompletableFuture<User> userFuture = asyncCache.get(userId, s -> userDao.getUser(s));
    // 获取公司研制体系部门列表
    CompletableFuture<List<String>> devDeptFuture =
            CompletableFuture.supplyAsync(() -> departmentDao.getDevDepartments());
    // 等用户信息、研制部门列表都拉取完结后,判别用户是否归于研制体系
    CompletableFuture<Boolean> combineResult =
            userFuture.thenCombine(devDeptFuture,
                    (user, devDepts) -> devDepts.contains(user.getDepartmentId()));
    // 等候履行完结,调用线程获取终究成果
    return combineResult.join();
}

在上述代码中,需求获取到用户概况与研制部门列表信息,然后判别用户对应的部门是否归于研制部门,然后判别员工是否为研制人员。全体选用异步编程的思路,并运用了Caffeine异步缓存的操作办法,完结了用户获取与研制部门列表获取这两个耗时操作并行的处理,提高全体处理功率。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

异步CacheLoader

异步处理的时分,Caffeine也支撑直接在创立的时分指定CacheLoader方针,然后生成支撑异步回源操作的AsyncLoadingCache缓存方针,然后在运用get办法获取成果的时分,也是回来的CompletableFuture异步封装类型,满意在异步编程场景下的运用。

public static void main(String[] args) {
    try {
        AsyncLoadingCache<String, User> asyncLoadingCache =
                Caffeine.newBuilder().maximumSize(1000L).buildAsync(key -> userDao.getUser(key));
        CompletableFuture<User> userCompletableFuture = asyncLoadingCache.get("123");
        System.out.println(userCompletableFuture.join());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

异步AsyncCacheLoader

除了上述这种办法,在创立的时分给定一个用于回源处理的CacheLoader之外,Caffeine还有一个buildAsync的重载版本,答应传入一个同样是支撑异步并行处理的AsyncCacheLoader方针。运用办法如下:

public static void main(String[] args) {
    try {
        AsyncLoadingCache<String, User> asyncLoadingCache =
                Caffeine.newBuilder().maximumSize(1000L).buildAsync(
                        (key, executor) -> CompletableFuture.supplyAsync(() -> userDao.getUser(key), executor)
                );
        CompletableFuture<User> userCompletableFuture = asyncLoadingCache.get("123");
        System.out.println(userCompletableFuture.join());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

与上一章节中的代码比对能够发现,不管是运用CacheLoader仍是AsyncCacheLoader方针,终究生成的缓存类型都是AsyncLoadingCache类型,运用的时分也并没有实质性的差异,两种办法的差异点仅在于传入buildAsync办法中的方针类型不同而已,运用的时分能够依据喜好自行选择。

进一步地,假如咱们测验将上面代码中的asyncLoadingCache缓存方针的具体类型打印出来,咱们会发现其具体类型可能是:

com.github.benmanes.caffeine.cache.BoundedLocalCache.BoundedLocalAsyncLoadingCache

而假如咱们在构造缓存方针的时分没有限制其最大容量信息,其构建出来的缓存方针类型还可能会是下面这个:

com.github.benmanes.caffeine.cache.UnboundedLocalCache.UnboundedLocalAsyncLoadingCache

与前面同步办法一样,咱们也能够看下这两个具体的缓存类型对应的UML类图关系:

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

能够看出,异步缓存不同类型终究都完结了同一个AsyncCache顶层接口类,而AsyncLoadingCache作为承继自AsyncCache的子类,除具有了AsyncCache的一切接口外,还额外扩展了部分的接口,以支撑未射中方针时自动运用指定的CacheLoader或者AysncCacheLoader方针去履行回源逻辑。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

小结回忆

好啦,关于Caffeine Cache的同步、异步数据回源操作原理与运用办法的论述,就介绍到这儿了。不知道小伙伴们是否对Caffeine Cache的回源机制有了全新的知道了呢?而关于Caffeine Cache,你是否有自己的一些主意与见解呢?欢迎谈论区一同交流下,等待和各位小伙伴们一同切磋、共同生长。

下一篇文章中,咱们将深入讲解下Caffeine改良过的异步数据驱赶处理完结,以及Caffeine支撑的多种不同的数据筛选驱赶机制和对应的实践运用。如有爱好,欢迎重视后续更新。

弥补阐明1

本文归于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏环绕缓存这个庞大出题进行打开论述,全方位、体系性地深度剖析各种缓存完结战略与原理、以及缓存的各种用法、各种问题应对战略,并一同讨论下缓存设计的哲学。

假如有爱好,也欢迎重视此专栏。

弥补阐明2

  • 关于本文中涉及的演示代码的完好示例,我现已整理并提交到github中,假如您有需求,能够自取:github.com/veezean/Jav…

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

我是悟道,聊技能、又不仅仅聊技能~

假如觉得有用,请点赞 + 重视让我感受到您的支撑。也能够重视下我的公众号【架构悟道】,获取更及时的更新。

等待与你一同讨论,一同生长为更好的自己。

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式