前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Guava Cache 使用小结

Guava Cache 使用小结

作者头像
kirito-moe
发布于 2022-03-04 04:46:34
发布于 2022-03-04 04:46:34
1.1K00
代码可运行
举报
运行总次数:0
代码可运行

闲聊

话说原创文章已经断更 2 个月了,倒也不是因为忙,主要还是懒。但是也感觉可以拿出来跟大家分享的技术点越来越少了,一方面主要是最近在从事一些“内部项目”的研发,纵使我很想分享,也没法搬到公众号 & 博客上来;一方面是一些我并不是很擅长的技术点,在我还是新手时,我敢于去写,而有了一定工作年限之后,反而有些包袱了,我的读者会不会介意呢?思来想去,我回忆起了写作的初心,不就是为了记录自己的学习过程吗?于是乎,我还是按照我之前的文风记录下了此文,以避免成为一名断更的博主。

以下是正文。

前言

“缓存”一直是我们程序员聊的最多的那一类技术点,诸如 Redis、Encache、Guava Cache,你至少会听说过一个。需要承认的是,无论是面试八股文的风气,还是实际使用的频繁度,Redis 分布式缓存的确是当下最为流行的缓存技术,但同时,从我个人的项目经验来看,本地缓存也是非常常用的一个技术点。

分析 Redis 缓存的文章很多,例如 Redis 雪崩、Redis 过期机制等等,诸如此类的公众号标题不鲜出现在我朋友圈的 timeline 中,但是分析本地缓存的文章在我的映像中很少。

在最近的项目中,有一位新人同事使用了 Guava Cache 来对一个 RPC 接口的响应进行缓存,我在 review 其代码时恰好发现了一个不太合理的写法,遂有此文。

本文将会介绍 Guava Cache 的一些常用操作:基础 API 使用,过期策略,刷新策略。并且按照我的写作习惯,会附带上实际开发中的一些总结。需要事先说明的是,我没有阅读过 Guava Cache 的源码,对其的介绍仅仅是一些使用经验或者最佳实践,不会有过多深入的解析。

先简单介绍一下 Guava Cache,它是 Google 封装的基础工具包 guava 中的一个内存缓存模块,它主要提供了以下能力:

  • 封装了缓存与数据源交互的流程,使得开发更关注于业务操作
  • 提供线程安全的存取操作(可以类比 ConcurrentHashMap)
  • 提供常用的缓存过期策略,缓存刷新策略
  • 提供缓存命中率的监控

基础使用

使用一个示例介绍 Guava Cache 的基础使用方法 -- 缓存大小写转换的返回值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private String fetchValueFromServer(String key) {
    return key.toUpperCase();
}

@Test
public void whenCacheMiss_thenFetchValueFromServer() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    assertEquals(0, cache.size());
    assertEquals("HELLO", cache.getUnchecked("hello"));
    assertEquals("HELLO", cache.get("hello"));
    assertEquals(1, cache.size());
}

使用 Guava Cache 的好处已经跃然于纸上了,它解耦了缓存存取与业务操作。CacheLoader 的 load 方法可以理解为从数据源加载原始数据的入口,当调用 LoadingCache 的 getUnchecked 或者 get方法时,Guava Cache 行为如下:

  • 缓存未命中时,同步调用 load 接口,加载进缓存,返回缓存值
  • 缓存命中,直接返回缓存值
  • 多线程缓存未命中时,A 线程 load 时,会阻塞 B 线程的请求,直到缓存加载完毕

注意到,Guava 提供了两个 getUnchecked 或者 get 加载方法,没有太大的区别,无论使用哪一个,都需要注意,数据源无论是 RPC 接口的返回值还是数据库,都要考虑访问超时或者失败的情况,做好异常处理。

预加载缓存

预加载缓存的常见使用场景:

  • 老生常谈的秒杀场景,事先缓存预热,将热点商品加入缓存;
  • 系统重启过后,事先加载好缓存,避免真实请求击穿缓存

Guava Cache 提供了 put 和 putAll 方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenPreloadCache_thenPut() {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    String key = "kirito";
    cache.put(key,fetchValueFromServer(key));

    assertEquals(1, cache.size());
}

操作和 HashMap 一模一样。

这里有一个误区,而那位新人同事恰好踩到了,也是我写这篇文章的初衷,请务必仅在预加载缓存这个场景使用 put,其他任何场景都应该使用 load 去触发加载缓存。看下面这个反面示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 注意这是一个反面示例
@Test
public void wrong_usage_whenCacheMiss_thenPut() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return "";
            }
        });

    String key = "kirito";
    String cacheValue = cache.get(key);
    if ("".equals(cacheValue)) {
        cacheValue = fetchValueFromServer(key);
        cache.put(key, cacheValue);
    }
    cache.put(key, cacheValue);

    assertEquals(1, cache.size());
}

这样的写法,在 load 方法中设置了一个空值,后续通过手动 put + get 的方式使用缓存,这种习惯更像是在操作一个 HashMap,但并不推荐在 Cache 中使用。在前面介绍过 get 配合 load 是由 Guava Cache 去保障了线程安全,保障多个线程访问缓存时,第一个请求加载缓存的同时,阻塞后续请求,这样的 HashMap 用法既不优雅,在极端情况下还会引发缓存击穿、线程安全等问题。

请务必仅仅将 put 方法用作预加载缓存场景。

缓存过期

前面的介绍使用起来依旧没有脱离 ConcurrentHashMap 的范畴,Cache 与其的第一个区别在“缓存过期”这个场景可以被体现出来。本节介绍 Guava 一些常见的缓存过期行为及策略。

缓存固定数量的值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenReachMaxSize_thenEviction() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().maximumSize(3).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    cache.get("one");
    cache.get("two");
    cache.get("three");
    cache.get("four");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("one"));
    assertEquals("FOUR", cache.getIfPresent("four"));
}

使用 ConcurrentHashMap 做缓存的一个最大的问题,便是我们没有简易有效的手段阻止其无限增长,而 Guava Cache 可以通过初始化 LoadingCache 的过程,配置 maximumSize ,以确保缓存内容不导致你的系统出现 OOM。

值得注意的是,我这里的测试用例使用的是除了 get 、getUnchecked 外的第三种获取缓存的方式,如字面意思描述的那样,getIfPresent 在缓存不存在时,并不会触发 load 方法加载数据源。

LRU 过期策略

依旧沿用上述的示例,我们在设置容量为 3 时,仅获悉 LoadingCache 可以存储 3 个值,却并未得知第 4 个值存入后,哪一个旧值需要淘汰,为新值腾出空位。实际上,Guava Cache 默认采取了 LRU 缓存淘汰策略。Least Recently Used 即最近最少使用,这个算法你可能没有实现过,但一定会听说过,在 Guava Cache 中 Used 的语义代表任意一次访问,例如 put、get。继续看下面的示例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenReachMaxSize_thenEviction() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().maximumSize(3).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    cache.get("one");
    cache.get("two");
    cache.get("three");
    // access one
    cache.get("one");
    cache.get("four");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("two"));
    assertEquals("ONE", cache.getIfPresent("one"));
}

注意此示例与上一节示例的区别:第四次 get 访问 one 后,two 变成了最久未被使用的值,当第四个值 four 存入后,淘汰的对象变成了 two,而不再是 one 了。

缓存固定时间

为缓存设置过期时间,也是区分 HashMap 和 Cache 的一个重要特性。Guava Cache 提供了expireAfterAccess、 expireAfterWrite 的方案,为 LoadingCache 中的缓存值设置过期时间。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenEntryIdle_thenEviction()
    throws InterruptedException, ExecutionException {

    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    cache.get("kirito");
    assertEquals(1, cache.size());

    cache.get("kirito");
    Thread.sleep(2000);

    assertNull(cache.getIfPresent("kirito"));
}

缓存失效

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenInvalidate_thenGetNull() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) {
                    return fetchValueFromServer(key);
                }
            });

    String name = cache.get("kirito");
    assertEquals("KIRITO", name);

    cache.invalidate("kirito");
    assertNull(cache.getIfPresent("kirito"));
}

使用 void invalidate(Object key) 移除单个缓存,使用 void invalidateAll() 移除所有缓存。

缓存刷新

缓存刷新的常用于使用数据源的新值覆盖缓存旧值,Guava Cache 提供了两类刷新机制:手动刷新和定时刷新。

手动刷新

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
cache.refresh("kirito");

refresh 方法将会触发 load 逻辑,尝试从数据源加载缓存。

需要注意点的是,refresh 方法并不会阻塞 get 方法,所以在 refresh 期间,旧的缓存值依旧会被访问到,直到 load 完毕,看下面的示例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenCacheRefresh_thenLoad()
    throws InterruptedException, ExecutionException {

    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws InterruptedException {
                Thread.sleep(2000);
                return key + ThreadLocalRandom.current().nextInt(100);
            }
        });

    String oldValue = cache.get("kirito");

    new Thread(() -> {
        cache.refresh("kirito");
    }).start();

    // make sure another refresh thread is scheduling
    Thread.sleep(500);

    String val1 = cache.get("kirito");

    assertEquals(oldValue, val1);

    // make sure refresh cache 
    Thread.sleep(2000);

    String val2 = cache.get("kirito");
    assertNotEquals(oldValue, val2);

}

其实任何情况下,缓存值都有可能和数据源出现不一致,业务层面需要做好访问到旧值的容错逻辑。

自动刷新

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenTTL_thenRefresh() throws ExecutionException, InterruptedException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key + ThreadLocalRandom.current().nextInt(100);
            }
        });

    String first = cache.get("kirito");
    Thread.sleep(1000);
    String second = cache.get("kirito");

    assertNotEquals(first, second);
}

和上节的 refresh 机制一样,refreshAfterWrite 同样不会阻塞 get 线程,依旧有访问旧值的可能性。

缓存命中统计

Guava Cache 默认情况不会对命中情况进行统计,需要在构建 CacheBuilder 时显式配置 recordStats

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenRecordStats_thenPrint() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().maximumSize(100).recordStats().build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return fetchValueFromServer(key);
            }
        });

    cache.get("one");
    cache.get("two");
    cache.get("three");
    cache.get("four");

    cache.get("one");
    cache.get("four");

    CacheStats stats = cache.stats();
    System.out.println(stats);
}
---
CacheStats{hitCount=2, missCount=4, loadSuccessCount=4, loadExceptionCount=0, totalLoadTime=1184001, evictionCount=0}

缓存移除的通知机制

在一些业务场景中,我们希望对缓存失效进行一些监测,或者是针对失效的缓存做一些回调处理,就可以使用 RemovalNotification 机制。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void whenRemoval_thenNotify() throws ExecutionException {
    LoadingCache<String, String> cache =
        CacheBuilder.newBuilder().maximumSize(3)
            .removalListener(
                cacheItem -> System.out.println(cacheItem + " is removed, cause by " + cacheItem.getCause()))
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) {
                    return fetchValueFromServer(key);
                }
            });

    cache.get("one");
    cache.get("two");
    cache.get("three");
    cache.get("four");
}
---
one=ONE is removed, cause by SIZE

removalListener 可以给 LoadingCache 增加一个回调处理器,RemovalNotification 实例包含了缓存的键值对以及移除原因。

Weak Keys & Soft Values

Java 基础中的弱引用和软引用的概念相信大家都学习过,这里先给大家复习一下

  • 软引用:如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足,就会回收这些对象。只要垃圾回收器没有回收它,该对象就可以被程序使用
  • 弱引用:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

在 Guava Cache 中,CacheBuilder 提供了 weakKeys、weakValues、softValues 三种方法,将缓存的键值对与 JVM 垃圾回收机制产生关联。

该操作可能有它适用的场景,例如最大限度的使用 JVM 内存做缓存,但依赖 GC 清理,性能可想而知会比较低。总之我是不会依赖 JVM 的机制来清理缓存的,所以这个特性我不敢使用,线上还是稳定性第一。

如果需要设置清理策略,可以参考缓存过期小结中的介绍固定数量和固定时间两个方案,结合使用确保使用缓存获得高性能的同时,不把内存打挂。

总结

本文介绍了 Guava Cache 一些常用的 API 、用法示例,以及需要警惕的一些使用误区。

在选择使用 Guava 时,我一般会结合实际使用场景,做出以下的考虑:

  1. 为什么不用 Redis? 如果本地缓存能够解决,我不希望额外引入一个中间件
  2. 如果保证缓存和数据源数据的一致性? 一种情况,我会在数据要求敏感度不高的场景使用缓存,所以短暂的不一致可以忍受;另外一些情况,我会在设置定期刷新缓存以及手动刷新缓存的机制。举个例子,页面上有一个显示应用 developer 列表的功能,而本地仅存储了应用名,developer 列表是通过一个 RPC 接口查询获取的,而由于对方的限制,该接口 qps 承受能力非常低,便可以考虑缓存 developer 列表,并配置 maximumSize 以及 expireAfterAccess。如果有用户在 developer 数据源中新增了数据,导致了数据不一致,页面也可以设置一个同步按钮,让用户去主动 refresh;或者,如果判断当前用户不在 developer 列表,也可以程序 refresh 一次。总之非常灵活,使用 Guava Cache 的 API 可以满足大多数业务场景的缓存需求。
  3. 为什么是 Guava Cache,它的性能怎么样? 我现在主要是出于稳定性考虑,项目一直在使用 Guava Cache。据说有比 Guava Cache 快的本地缓存,但那点性能我的系统不是特别关心。

- END -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kirito的技术分享 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Python 列表、元组、字典及集合操作
注意:当索引超出范围时,Python会报一个IndexError错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(list1) - 1。
py3study
2020/01/19
1.5K0
python列表、元组、字典
列表是由一序列特定顺序排列的元素组成的。可以把字符串,数字,字典等都可以任何东西加入到列表中,列表中的元素之间没有任何关系。列表也是自带下标的,默认也还是从0开始。列表常用方括号表示,即:[],元素用逗号隔开。
py3study
2020/01/15
1.3K0
刚才,我发现了Python强大的内置模块collections
collections 是 Python 的一个内置模块,所谓内置模块的意思是指 Python 内部封装好的模块,无需安装即可直接使用。
Wu_Candy
2022/07/05
3130
Python字典和集合
1 字典字典和列表类似,同样是可变序列,不过与列表不同,字典是无序的。主要特征解释通过键而不是通过索引来读取元素字典类型有时也称为关联数组或者散列表(hash)。它是通过键将一系列的值联系起来的,这样就可以通过键从字典中获取指定项,但不能通过索引来获取。字典是任意数据类型的无序集合和列表、元组不同,通常会将索引值 0 对应的元素称为第一个元素,而字典中的元素是无序的。字典是可变的,并且可以任意嵌套字典可以在原处增长或者缩短(无需生成一个副本),并且它支持任意深度的嵌套,即字典存储的值也可以是列表或其它的字典
虫无涯
2023/02/02
8640
python-for-data-python基础
本文主要是对Python的数据结构进行了一个总结,常见的数据结构包含:列表list、元组tuple、字典dict和集合set。
皮大大
2021/03/01
1.3K0
python-for-data-python基础
Python黑帽编程2.3 字符串、列表、元组、字典和集合
本节要介绍的是Python里面常用的几种数据结构。通常情况下,声明一个变量只保存一个值是远远不够的,我们需要将一组或多组数据进行存储、查询、排序等操作,本节介绍的Python内置的数据结构可以满足大多数情况下的需求。这一部分的知识点比较多,而且较为零散,需要认真学习。 2.3.1 字符串 字符串是 Python 中最常用的数据类型。我们可以使用引号('或")来创建字符串。 创建字符串很简单,只要为变量分配一个值即可。例如: var1 ='Hello World!' var2 ="Python Runoob
用户1631416
2018/04/12
1.8K0
Python黑帽编程2.3 字符串、列表、元组、字典和集合
Python基础语法(四)—列表、元组、字典、集合、字符串
列表 基本概念 列表是有序的元素集合,所有元素放在一对中括号中,用逗号隔开,没有长度限制; 列表索引值以0为开始值,-1为从未尾的开始位置。 列表可以使用+操作符进行拼接,使用*表示重复。 当列表元素增加或删除时,列表对象自动进行扩展或收缩内存,保证元素之间没有缝隙; 列表中的元素可以是不同类型的 列表的使用方式 list = ["zeruns","blog","blog.zeruns.tech",9527,[0,1,2,[1,2]]]#创建一个列表,一个列表里可以有多种数据类型,甚至可以嵌套列表来做二或三
zeruns
2020/03/23
2.6K0
python基础语法——函数、列表、元组和字典
本文基于pycharm编译器,也可以使用Anaconda 里的编译器,将讲解一些python的一些基础语法知识,是对上篇文章的补充,可以和我写的python数据分析——Python语言基础(数据结构基础)结合起来看,有些知识点可能在这篇文章写的不是很全面。
鲜于言悠
2024/03/20
2230
python基础语法——函数、列表、元组和字典
【Python】python创建字典(dict)的几种方法(含代码示例)
字典(Dictionary)是Python中一种非常灵活的数据结构,用于存储键值对(key-value pairs)。在Python中创建字典有多种方法,每种方法都有其特定的使用场景和优势。
程序员洲洲
2024/06/09
1.8K0
【Python】python创建字典(dict)的几种方法(含代码示例)
Python复习 一
从上边可以看出,list直接复制和list[:]分片复制的结果一样,但其实暗藏心急哦!
Mirror王宇阳
2020/11/13
1.3K0
Python3快速入门(三)——Pyth
Python3 中有六种标准数据类型: A、Number(数字) B、String(字符串) C、List(列表) D、Tuple(元组) E、Set(集合) F、Dictionary(字典) Python3 的六种标准数据类型中,Number(数字)、String(字符串)、Tuple(元组)是不可变的,List(列表)、Dictionary(字典)、Set(集合)是可变的。
py3study
2020/01/06
3.7K0
2.0 Python 数据结构与类型
数据类型是编程语言中的一个重要概念,它定义了数据的类型和提供了特定的操作和方法。在 python 中,数据类型的作用是将不同类型的数据进行分类和定义,例如数字、字符串、列表、元组、集合、字典等。这些数据类型不仅定义了数据的类型,还为数据提供了一些特定的操作和方法,例如字符串支持连接和分割,列表支持排序和添加元素,字典支持查找和更新等。因此,选择合适的数据类型是 python 编程的重要组成部分。
王瑞MVP
2023/08/11
6080
python-元组,字典,列表
由于会处理一些json数据,内部字典,列表,元租傻傻分不清,所以这里总结一下他们的特点,便于提取数据 想要知道跟多看官方文档,很详细 https://www.runoob.com/python/python-lists.html 我是看了官方文档后总结后我自己的
全栈程序员站长
2021/05/19
1.2K0
python列表、字典、元组、集合学习笔记
列 表 列表在python里是有序集合对象类型。 列表里的对象可以是任何对象:数字,字符串,列表或者字典,元组。与字符串不同,列表是可变对象,支持原处修改的操作 python的列表是:
没有故事的陈师傅
2019/07/28
2.3K0
Python教程第3章 | 集合(List列表、Tuple元组、Dict字典、Set)
前面我们学习了基本数据类型和变量,现在我们学习Python的四种集合,列表(List)和元组(tuple),字典(Dict),无序列表(Set)
仲君Johnny
2024/01/24
1.1K0
Python教程第3章 | 集合(List列表、Tuple元组、Dict字典、Set)
python学习笔记(2)python数据类型
整型(Int) - 通常被称为是整型或整数,是正或负整数,不带小数点。Python3 整型是没有限制大小的,可以当作 Long 类型使用,所以 Python3 没有 Python2 的 Long 类型。
大数据小禅
2021/08/16
8640
【利用Python进行数据分析】3-Python的数据结构、函数和文件
元组是一个固定长度,不可改变的Python序列对象,创建元组的最简单方式,是用逗号分隔一列值。当用复杂的表达式定义元组,最好将值放到圆括号内。
用户7886150
2020/12/24
8860
一天快速入门python
Python 是由Guido Van Rossum在 90 年代早期设计,现在是最常用的编程语言之一。特别是人工智能的火热,再加之它的语法简洁且优美,实乃初学者入门AI必备的编程语言。
yuquanle
2019/05/27
8590
python文档:数据结构(列表的特性,del语句,元组,集合,循环技巧)字典,
本章节将详细介绍一些您已经了解的内容,并添加了一些新内容。 5.1. 列表的更多特性 列表数据类型还有很多的方法。这里是列表对象方法的清单:
川川菜鸟
2021/10/18
1.6K0
Python元组与字典
例:d2=dict(name="jerry",age="45",gender="m")
py3study
2020/01/10
9230
推荐阅读
相关推荐
Python 列表、元组、字典及集合操作
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 闲聊
  • 前言
  • 基础使用
  • 预加载缓存
  • 缓存过期
    • 缓存固定数量的值
    • LRU 过期策略
    • 缓存固定时间
  • 缓存失效
  • 缓存刷新
    • 手动刷新
    • 自动刷新
  • 缓存命中统计
  • 缓存移除的通知机制
  • Weak Keys & Soft Values
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档