学习
实践
活动
专区
工具
TVP
写文章
专栏首页架构悟道重新认识下JVM级别的本地缓存框架Guava Cache(2)——深入解读其容量限制与数据淘汰策略
原创

重新认识下JVM级别的本地缓存框架Guava Cache(2)——深入解读其容量限制与数据淘汰策略

大家好,又见面了。


本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。


通过《重新认识下JVM级别的本地缓存框架Guava Cache——优秀从何而来》一文,我们知道了Guava Cache作为JVM级别的本地缓存组件的诸多暖心特性,也一步步地学习了在项目中集成并使用Guava Cache进行缓存相关操作。Guava Cache作为一款优秀的本地缓存组件,其内部很多实现机制与设计策略,同样值得开发人员深入的掌握与借鉴。

作为系列专栏,本篇文章我们将在上一文的基础上,继续探讨下Guava Cache对于缓存容量限制数据清理相关的使用与设计机制,进而让我们在项目中使用起来可以更加的游刃有余,解锁更多使用技巧。

容量限制时的Size与Weight区别

弄清Size与Weight

Guava Cache提供了对缓存总量的限制,并且支持从两个维度进行限制,这里我们首先要厘清sizeweight两个概念的区别与联系。

  • 限制缓存条数size
public Cache<String, User> createUserCache() {
    return CacheBuilder.newBuilder().maximumSize(10000L).build();
}
  • 限制缓存权重weight
public Cache<String, String> createUserCache() {
    return CacheBuilder.newBuilder()
            .maximumWeight(50000)
            .weigher((key, value) -> (int) Math.ceil(value.length() / 1000))
            .build();
    }

一般而言,我们限制容器的容量的初衷,是为了防止内存占用过大导致内存溢出,所以本质上是限制内存的占用量。从实现层面,往往会根据总内存占用量与预估每条记录字节数进行估算,将其转换为对缓存记录条数的限制。这种做法相对简单易懂,但是对于单条缓存记录占用字节数差异较大的情况下,会导致基于条数控制的结果不够精准

比如:

需要限制缓存最大占用500M总量,缓存记录可能大小范围是1k~100k,按照每条50k进行估算,设定缓存容器最大容量为限制最大容量1w条。如果存储的都是1k大小的记录,则内存总占用量才10M(内存没有被有效利用起来);若都存储的是100k大小的记录,又会导致内存占用为1000M,远大于预期的内存占用量(容易造成内存溢出)。

为了解决这个问题,Guava Cache中提供了一种相对精准的控制策略,即基于权重的总量控制,根据一定的规则,计算出每条value记录所占的权重值,然后以权重值进行总量的计算。

还是上面的例子,我们按照权重进行设定,假定1k对应基础权重1,则100k可转换为权重100。这样一来:

限制缓存最大占用500M1k对应权重1,Nk代表权重N,则我们可以限制总权重为50w。这样假如存储的都是1k的记录,则最多可以缓存5w条记录;而如果都是100k大小的记录,则最多仅可以缓存5000条记录。根据存储数据的大小不同,最大存储的记录条数也不相同,但是最终占用的总体量可以实现基本吻合。

所以,基于weight权重的控制方式,比较适用于这种对容器体量控制精度严格诉求的场景,可以在创建容器的时候指定每条记录的权重计算策略(比如基于字符串长度或者基于bytes数组长度进行计算权重)。

使用约束说明

在实际使用中,这几个参数之间有一定的使用约束,需要特别注意一下:

  • 如果没有指定weight实现逻辑,则使用maximumSize来限制最大容量,按照容器中缓存记录的条数进行限制;这种情况下,即使设定了maximumWeight也不会生效。
  • 如果指定了weight实现逻辑,则必须使用 maximumWeight 来限制最大容量,按照容器中每条缓存记录的weight值累加后的总weight值进行限制。

看下面的一个反面示例,指定了weighter和maximumSize,却没有指定 maximumWeight属性:

public static void main(String[] args) {
    try {
        Cache<String, String> cache = CacheBuilder.newBuilder()
            .weigher((key, value) -> 2)
            .maximumSize(2)
            .build();
        cache.put("key1", "value1");
        cache.put("key2", "value2");
        System.out.println(cache.size());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行的时候,会报错,提示weighter和maximumSize不可以混合使用:

java.lang.IllegalStateException: maximum size can not be combined with weigher
	at com.google.common.base.Preconditions.checkState(Preconditions.java:502)
	at com.google.common.cache.CacheBuilder.maximumSize(CacheBuilder.java:484)
	at com.veezean.skills.cache.guava.CacheService.main(CacheService.java:205)

Guava Cache淘汰策略

为了简单描述,我们将数据从缓存容器中移除的操作统称数据淘汰。按照触发形态不同,我们可以将数据的清理与淘汰策略分为被动淘汰主动淘汰两种。

被动淘汰

  • 基于数据量(size或者weight)

当容器内的缓存数量接近(注意是接近、而非达到)设定的最大阈值的时候,会触发guava cache的数据清理机制,会基于LRU或FIFO删除一些不常用的key-value键值对。这种方式需要在创建容器的时候指定其maximumSize或者maximumWeight,然后才会基于size或者weight进行判断并执行上述的清理操作。

看下面的实验代码:

public static void main(String[] args) {
    try {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(2)
                .removalListener(notification -> {
                    System.out.println("---监听到缓存移除事件:" + notification);
                })
                .build();
        System.out.println("put放入key1");
        cache.put("key1", "value1");
        System.out.println("put放入key2");
        cache.put("key2", "value1");
        System.out.println("put放入key3");
        cache.put("key3", "value1");
        System.out.println("put操作后,当前缓存记录数:" + cache.size());
        System.out.println("查询key1对应值:" + cache.getIfPresent("key1"));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上面代码中,没有设置数据的过期时间,理论上数据是长期有效、不会被过期删除。为了便于测试,我们设定缓存最大容量为2条记录,然后往缓存容器中插入3条记录,观察下输出结果如下:

put放入key1
put放入key2
put放入key3
---监听到缓存移除事件:key1=value1
put操作后,当前缓存记录数:2
查询key1对应值:null

从输出结果可以看到,即使数据并没有过期,但在插入第3条记录的时候,缓存容器还是自动将最初写入的key1记录给移除了,挪出了空间用于新的数据的插入。这个就是因为触发了Guava Cache的被动淘汰机制,以确保缓存容器中的数据量始终是在可控范围内。

  • 基于过期时间

Guava Cache支持根据创建时间或者根据访问时间来设定数据过期处理,实际使用的时候可以根据具体需要来选择对应的方式。

过期策略

具体说明

创建过期

基于缓存记录的插入时间判断。比如设定10分钟过期,则记录加入缓存之后,不管有没有访问,10分钟时间到则

访问过期

基于最后一次的访问时间来判断是否过期。比如设定10分钟过期,如果缓存记录被访问到,则以最后一次访问时间重新计时;只有连续10分钟没有被访问的时候才会过期,否则将一直存在缓存中不会被过期。

看下面的实验代码:

public static void main(String[] args) {
    try {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1L, TimeUnit.SECONDS)
                .recordStats()
                .build();
        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");
        System.out.println("put操作后,当前缓存记录数:" + cache.size());
        System.out.println("查询key1对应值:" + cache.getIfPresent("key1"));
        System.out.println("统计信息:" + cache.stats());
        System.out.println("-------sleep 等待超过过期时间-------");
        Thread.sleep(1100L);
        System.out.println("执行key1查询操作:" + cache.getIfPresent("key1"));
        System.out.println("当前缓存记录数:" + cache.size());
        System.out.println("当前统计信息:" + cache.stats());
        System.out.println("剩余数据信息:" + cache.asMap());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在实验代码中,我们设置了缓存记录1s有效期,然后等待其过期之后查看其缓存中数据情况,代码执行结果如下:

put操作后,当前缓存记录数:3
查询key1对应值:value1
统计信息:CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
-------sleep 等待超过过期时间-------
执行key1查询操作:null
当前缓存记录数:1
当前统计信息:CacheStats{hitCount=1, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=2}
剩余数据信息:{}

从结果中可以看出,超过过期时间之后,再次执行get操作已经获取不到已过期的记录,相关记录也被从缓存容器中移除了。请注意,上述代码中我们特地是在过期之后执行了一次get请求然后才去查看缓存容器中存留记录数量与统计信息的,主要是因为Guava Cache的过期数据淘汰是一种被动触发技能。

当然,细心的小伙伴可能会发现上面的执行结果有一个“问题”,就是前面一起put写入了3条记录,等到超过过期时间之后,只移除了2条过期数据,还剩了一条记录在里面?但是去获取剩余缓存里面的数据的时候又显示缓存里面是空的?

Guava Cache作为一款优秀的本地缓存工具包,是不可能有这么个大的bug遗留在里面的,那是什么原因呢?

这个现象其实与Guava Cache的缓存淘汰实现机制有关系,前面说过Guava Cache的过期数据清理是一种被动触发技能,我们看下getIfPresent方法对应的实现源码,可以很明显的看出每次get请求的时候都会触发一次cleanUp操作:

为了实现高效的多线程并发控制,Guava Cache采用了类似ConcurrentHashMap一样的分段锁机制,数据被分为了不同分片,每个分片同一时间只允许有一个线程执行写操作,这样降低并发锁争夺的竞争压力。而上面代码中也可以看出,执行清理的时候,仅针对当前查询的记录所在的Segment分片执行清理操作,而其余的分片的过期数据并不会触发清理逻辑 —— 这个也就是为什么前面例子中,明明3条数据都过期了,却只清理掉了其中的2条的原因。

为了验证上述的原因说明,我们可以在创建缓存容器的时候将concurrencyLevel设置为允许并发数为1,强制所有的数据都存放在同一个分片中:

public static void main(String[] args) {
    try {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1L, TimeUnit.SECONDS)
                .concurrencyLevel(1)  // 添加这一约束,强制所有数据放在一个分片中
                .recordStats()
                .build();

                // ...省略其余逻辑,与上一段代码相同

    } catch (Exception e) {
        e.printStackTrace();
    }
}

重新运行后,从结果可以看出,这一次3条过期记录全部被清除了。

put操作后,当前缓存记录数:3
查询key1对应值:value1
统计信息:CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
-------sleep 等待超过过期时间-------
执行key1查询操作:null
当前缓存记录数:0
当前统计信息:CacheStats{hitCount=1, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=3}
剩余数据信息:{}

在实际的使用中,我们倒也无需过于关注数据过期是否有被从内存中真实移除这一点,因为Guava Cache会在保证业务数据准确的情况下,尽可能的兼顾处理性能,在该清理的时候,自会去执行对应的清理操作,所以也无需过于担心。

  • 基于引用

基于引用回收的策略,核心是利用JVM虚拟机的GC机制来达到数据清理的目的。按照JVM的GC原理,当一个对象不再被引用之后,便会执行一系列的标记清除逻辑,并最终将其回收释放。这种实际使用的较少,此处不多展开。

主动淘汰

上述通过总体容量限制或者通过过期时间约束来执行的缓存数据清理操作,是属于一种被动触发的机制。

实际使用的时候也会有很多情况,我们需要从缓存中立即将指定的记录给删除掉。比如执行删除或者更新操作的时候我们就需要删除已有的历史缓存记录,这种情况下我们就需要主动调用 Guava Cache提供的相关删除操作接口,来达到对应诉求。

接口名称

含义描述

invalidate(key)

删除指定的记录

invalidateAll(keys)

批量删除给定的记录

invalidateAll()

清空整个缓存容器

小结回顾

好啦,关于Guava Cache中的容量限制与数据淘汰策略,就介绍到这里了。关于本章的内容,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

📣 补充说明1

本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。如果有兴趣,也欢迎关注此专栏。

📣 补充说明2

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

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

登录 后参与评论
0 条评论

相关文章

  • 解读JVM级别本地缓存Caffeine青出于蓝的要诀3 —— 讲透Caffeine的数据驱逐淘汰机制与用法

    上一篇文章中,我们聊了下Caffeine的同步、异步的数据回源方式。本篇文章我们再一起研讨下Caffeine的多种不同的数据淘汰驱逐机制,以及对应的实际使用。

    架构悟道
  • 重新认识下JVM级别的本地缓存框架Guava Cache——优秀从何而来

    不知不觉,这已经是《深入理解缓存原理与实战设计》系列专栏的第6篇文章了。经过前面5篇文章的铺垫,我们系统且全面的介绍了缓存相关的概念与典型问题,也手动实操了如何...

    架构悟道
  • 解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

    在前面的几篇文章中,我们一起聊了下本地缓存的动手实现、本地缓存相关的规范等,也聊了下Google的Guava Cache的相关原理与使用方式。比较心急的小伙伴已...

    架构悟道
  • 重新认识下JVM级别的本地缓存框架Guava Cache(3)——探寻实现细节与核心机制

    通过《重新认识下JVM级别的本地缓存框架Guava Cache——优秀从何而来》一文,我们知道了Guava Cache作为JVM级别的本地缓存组件的诸多暖心特性...

    架构悟道
  • 大型分布式系统中的缓存架构

    CDN(Content Delivery Network 内容分发网络)的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中...

    搜云库技术团队
  • JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来 —— 感受来自Ehcache的强大实力

    作为《深入理解缓存原理与实战设计》系列专栏,前面几篇文章中我们详细的介绍与探讨了Guava Cache与Caffeine的实现、特性与使用方式。提到JAVA本地...

    架构悟道
  • 缓存那些事儿之【本地缓存篇】

    一般来说,一个业务平台系统的整体流程可以基本概括为如下图所示,用户请求从UI(浏览器或者客户端)到网络转发,经过应用服务的业务逻辑处理,再到存储(文件系统或数据...

    用户2991389
  • 快速掌握:大型分布式系统中的缓存架构

    CDN(Content Delivery Network 内容分发网络)的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中...

    数据和云
  • Caffeine缓存 最快缓存 内存缓存

    Caffeine是在Guava Cache的基础上做一层封装,性能有明显提高,二者同属于内存级本地缓存。使用Caffeine后无需使用Guava Cache,从...

    赛先生和泰先生
  • Guava Cache 使用小结

    闲聊 话说原创文章已经断更 2 个月了,倒也不是因为忙,主要还是懒。但是也感觉可以拿出来跟大家分享的技术点越来越少了,一方面主要是最近在从事一些“内部项目”的研...

    kirito-moe
  • 玩转EhCache之最简单的缓存框架

    Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cach...

    小程故事多
  • 缓存 | 从本地缓存到分布式缓存, Guava, Caffeine, Memcached, Redis

    在程序设计中,经常能听到的就是以时间换空间和以空间换时间。缓存作为一种能加快程序性能的银弹,它是典型的后者(以空间换时间).

    双鬼带单
  • 缓存之王Caffeine Cache,性能比Guava更强,命中率更高!

    在项目开发中,为提升系统性能,减少 IO 开销,本地缓存是必不可少的。最常见的本地缓存是 Guava 和 Caffeine,本篇文章将为大家介绍 Caffein...

    用户2781897
  • 必知必会:关于缓存的一些重要概念

    很多朋友,只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。

    架构之家
  • 开源的高性能本地缓存-Caffeine

    在将本地缓存前你肯定在想,本地缓存有么好讲的,不就是一个map么。把要缓存的数据存入map中,自己就能实现。但是这里有几个点我们要考虑:

    Lvshen
  • 干掉 GuavaCache:Caffeine 才是本地缓存的王

    话说,中间件的选择上,Spring(SpringBoot)一直是业界的风向标。比如Spring一直使用「Jackson」,而没有使用Gson和fastjson。...

    程序猿DD
  • 技术 | 小白都能看懂的缓存入门

    缓存是程序员必须了解的技术,无论是前端、后端还是客户端,大到复杂的系统架构,小到 CPU 或是芯片,都少不了缓存的影子。

    程序员鱼皮
  • 理解分布式系统中的缓存架构(上)

    CDN(Content Delivery Network 内容分发网络)的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中...

    lyb-geek
  • guava cache 用法详解

    在计算机领域的各个场景中,缓存都是一个非常常用的技术手段。通过高性能的缓存暂时存储重要的数据,可以有效提升整个系统的性能。

    用户3147702

扫码关注腾讯云开发者

领取腾讯云代金券