本地缓存高性能之王Caffeine

简介: 本地缓存高性能之王Caffeine

前言

随着互联网的高速发展,市面上也出现了越来越多的网站和app。我们判断一个软件是否好用,用户体验就是一个重要的衡量标准。比如说我们经常用的微信,打开一个页面要十几秒,发个语音要几分钟对方才能收到。相信这样的软件大家肯定是都不愿意用的。软件要做到用户体验好,响应速度快,缓存就是必不可少的一个神器。缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等。说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经它的性能是非常不错的。正所谓长江后浪推前浪,前浪被拍在沙滩上。我们就来介绍一个比Guava Cache性能更高的缓存框架:Caffeine

Tips: Spring5(SpringBoot2)开始用Caffeine取代guava.详见官方信息SPR-13797
https://jira.spring.io/browse/SPR-13797

官方性能比较

以下测试都是基于jmh测试的,官网地址
测试为什么要基于jmh测试,可以参考知乎上R回答

在HotSpot VM上跑microbenchmark切记不要在main()里跑循环计时就完事。这是典型错误。重要的事情重复三遍:请用JMH,请用JMH,请用JMH。除非非常了解HotSpot的实现细节,在main里这样跑循环计时得到的结果其实对一般程序员来说根本没有任何意义,因为无法解释。
  • 8个线程读,100%的读操作

  • 6个线程读,2个线程写,也就是75%的读操作,25%的写操作。

  • 8个线程写,100%的写操作

对比结论

可以从数据看出来Caffeine的性能都比Guava要好。然后Caffeine的API的操作功能和Guava是基本保持一致的,并且 Caffeine为了兼容之前是Guava的用户,做了一个Guava的Adapter给大家使用也是十分的贴心。

如何使用

  • 在 pom.xml 中添加 caffeine 依赖
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.2</version>
</dependency>

创建对象

 Cache<String, Object> cache = Caffeine.newBuilder()
                .initialCapacity(100)//初始大小
                .maximumSize(200)//最大数量
                .expireAfterWrite(3, TimeUnit.SECONDS)//过期时间
                .build();

创建参数介绍

  • initialCapacity: 初始的缓存空间大小
  • maximumSize: 缓存的最大数量
  • maximumWeight: 缓存的最大权重
  • expireAfterAccess: 最后一次读或写操作后经过指定时间过期
  • expireAfterWrite: 最后一次写操作后经过指定时间过期
  • refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存
  • weakKeys: 打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开发统计功能

    注意:

expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。maximumSize和maximumWeight不可以同时使用。

添加数据

Caffeine 为我们提供了手动、同步和异步这几种填充策略。
下面我们来演示下手动填充策略吧,其他几种如果大家感兴趣的可以去官网了解下

  Cache<String, String> cache = Caffeine.newBuilder()
                .build();
        cache.put("java金融", "java金融");
        System.out.println(cache.getIfPresent("java金融"));

自动添加(自定义添加函数)

  public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .build();
        // 1.如果缓存中能查到,则直接返回
        // 2.如果查不到,则从我们自定义的getValue方法获取数据,并加入到缓存中
        String val = cache.get("java金融", k -> getValue(k));
        System.out.println(val);
    }
    /**
     * 缓存中找不到,则会进入这个方法。一般是从数据库获取内容
     * @param k
     * @return
     */
    private static String getValue(String k) {
        return k + ":value";
    }

过期策略

Caffeine 为我们提供了三种过期策略
,分别是基于大小(size-based)、基于时间(time-based)、基于引用(reference-based)

基于大小(size-based)
      LoadingCache<String, String> cache = Caffeine.newBuilder()
                // 最大容量为1
                .maximumSize(1)
                .build(k->getValue(k));
        cache.put("java金融1","java金融1");
        cache.put("java金融2","java金融2");
        cache.put("java金融3","java金融3");
        cache.cleanUp();
        System.out.println(cache.getIfPresent("java金融1"));
        System.out.println(cache.getIfPresent("java金融2"));
        System.out.println(cache.getIfPresent("java金融3"));

运行结果如下:淘汰了两个只剩下一个。

null
null
java金融3
基于时间(time-based)

Caffeine提供了三种定时驱逐策略:

expireAfterWrite(long, TimeUnit)
  • 在最后一次写入缓存后开始计时,在指定的时间后过期。
  LoadingCache<String, String> cache =  Caffeine.newBuilder()
                // 最大容量为1
                .maximumSize(1)
                .expireAfterWrite(3, TimeUnit.SECONDS)
                .build(k->getValue(k));
        cache.put("java金融","java金融");
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));

运行结果第三秒的时候取值为空。

java金融
java金融
null
expireAfterAccess
  • 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
LoadingCache<String, String> cache =  Caffeine.newBuilder()
                // 最大容量为1
                .maximumSize(1)
                .expireAfterAccess(3, TimeUnit.SECONDS)
                .build(k->getValue(k));
        cache.put("java金融","java金融");
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java金融"));
        Thread.sleep(3001);
        System.out.println(cache.getIfPresent("java金融"));

运行结果:读和写都没有的情况下,3秒后才过期,然后就输出了null。

java金融
java金融
java金融
null
expireAfter(Expiry)
  • 在expireAfter中需要自己实现Expiry接口,这个接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之后多久过期。注意这个是和expireAfterAccess、expireAfterAccess是互斥的。这里和expireAfterAccess、expireAfterAccess不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,获取具体的过期时间。
 LoadingCache<String, String> cache = Caffeine.newBuilder()
                // 最大容量为1
                .maximumSize(1)
                .removalListener((key, value, cause) ->
                        System.out.println("key:" + key + ",value:" + value + ",删除原因:" + cause))
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return currentTime;
                    }
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }
                })
                .build(k -> getValue(k));

删除

  • 单个删除:Cache.invalidate(key)
  • 批量删除:Cache.invalidateAll(keys)
  • 删除所有缓存项:Cache.invalidateAll

总结

本文只是对Caffeine的一个简单使用的介绍,它还有很多不错的东西,比如缓存监控、事件监听、W-TinyLFU算法(高命中率、低内存占用)感兴趣的同学可以去官网查看。

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

参考

https://www.itcodemonkey.com/article/9498.html
https://juejin.im/post/5dede1f2518825121f699339
https://www.cnblogs.com/CrankZ/p/10889859.html
https://blog.csdn.net/hy245120020/article/details/78080686
https://github.com/ben-manes/caffeine

目录
相关文章
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
4月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
102 1
|
4月前
|
缓存 Java Spring
Java本地高性能缓存实践问题之Caffeine中设置刷新机制的问题如何解决
Java本地高性能缓存实践问题之Caffeine中设置刷新机制的问题如何解决
115 1
|
4月前
|
缓存 NoSQL Java
Redis深度解析:解锁高性能缓存的终极武器,让你的应用飞起来
【8月更文挑战第29天】本文从基本概念入手,通过实战示例、原理解析和高级使用技巧,全面讲解Redis这一高性能键值对数据库。Redis基于内存存储,支持多种数据结构,如字符串、列表和哈希表等,常用于数据库、缓存及消息队列。文中详细介绍了如何在Spring Boot项目中集成Redis,并展示了其工作原理、缓存实现方法及高级特性,如事务、发布/订阅、Lua脚本和集群等,帮助读者从入门到精通Redis,大幅提升应用性能与可扩展性。
86 0
|
4月前
|
存储 缓存 Java
Java本地高性能缓存实践问题之如何定义Caffeine的缓存
Java本地高性能缓存实践问题之如何定义Caffeine的缓存
|
4月前
|
缓存 Java
Java本地高性能缓存实践问题之Caffeine缓存库中基于时间设置驱逐策略的问题如何解决
Java本地高性能缓存实践问题之Caffeine缓存库中基于时间设置驱逐策略的问题如何解决
|
27天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
37 5
|
3月前
|
存储 消息中间件 缓存
本地缓存Caffeine系列(三)
本地缓存Caffeine系列(三)
|
3月前
|
存储 缓存 NoSQL
本地缓存Caffeine系列(一)
本地缓存Caffeine系列(一)
|
3月前
|
缓存 监控 Java
造轮子能力大提升:基于SpringBoot打造高性能缓存组件
在快节奏的软件开发领域,"不重复造轮子" 常常被视为提高效率的金科玉律。然而,在某些特定场景下,定制化的高性能缓存组件却是提升系统性能、优化用户体验的关键。今天,我们将深入探讨如何利用SpringBoot框架,从零开始打造一款符合项目需求的高性能缓存组件,分享我在这一过程中的技术心得与学习体会。
74 6