#cache #async-context #high-level

moka

一个受 Java Caffeine 启发的快速并发缓存库

56 个版本

0.12.8 2024 年 7 月 7 日
0.12.5 2024 年 1 月 29 日
0.12.2 2023 年 12 月 28 日
0.12.1 2023 年 10 月 3 日
0.1.0-alpha.1 2020 年 10 月 22 日

缓存 中排名第 3

Download history 139922/week @ 2024-05-04 158508/week @ 2024-05-11 142746/week @ 2024-05-18 149397/week @ 2024-05-25 190999/week @ 2024-06-01 179471/week @ 2024-06-08 158533/week @ 2024-06-15 159157/week @ 2024-06-22 155616/week @ 2024-06-29 200026/week @ 2024-07-06 200918/week @ 2024-07-13 206466/week @ 2024-07-20 204004/week @ 2024-07-27 196516/week @ 2024-08-03 251876/week @ 2024-08-10 221037/week @ 2024-08-17

每月下载量 908,268
192 个 crate(98 个直接)中使用

MIT/Apache

1MB
22K SLoC

Moka

GitHub Actions crates.io release docs dependency status codecov license FOSSA Status

注意 v0.12.0 在 API 和内部行为上进行了重大破坏性更改。请阅读 MIGRATION-GUIDE.md 获取详细信息。


Moka 是一个快速的 Rust 并发缓存库。Moka 受 Java 的 Caffeine 缓存库的启发。

Moka 在哈希表之上提供了缓存实现。它们支持检索的全并发和更新的高预期并发性。

所有缓存都会使用一个条目替换算法来尝试对哈希表进行最佳努力限制,以确定在容量超出时哪些条目将被驱逐。

特性

Moka 提供了一个丰富且灵活的功能集,同时保持高命中率和高并发访问级别。

  • 线程安全的、高度并发的内存缓存实现
    • 同步缓存,可以在操作系统线程之间共享。
    • 异步(对 futures 有感知)的缓存。
  • 缓存可以通过以下之一进行限制
    • 条目数量的最大值。
    • 条目的总加权大小。(大小感知驱逐)
  • 通过使用受 Caffeine 启发的条目替换算法来维护接近最优的命中率
  • 支持过期策略
    • 生存时间。
    • 空闲时间。
    • 每个条目可变过期。
  • 支持驱逐监听器,当条目从缓存中删除时将被调用的回调函数。

为您的用例选择合适的缓存

没有缓存实现适合所有用例。Moka 是一个复杂的软件,可能不适合您的用例。有时,像 Mini MokaQuick Cache 这样的简单缓存可能更适合。

下表显示了不同缓存实现之间的权衡

功能 Moka v0.12 Mini Moka v0.10 Quick Cache v0.3
线程安全、同步缓存
线程安全、异步缓存
非并发缓存
受最大条目数限制
受条目总加权大小限制
接近最优命中率 ✅ TinyLFU ✅ TinyLFU ✅ CLOCK-Pro
每个键,原子插入。(例如 get_with 方法)
缓存级别的过期策略(生存时间和空闲时间)
每个条目的可变过期时间
驱逐监听器
无锁,并发迭代器
分片锁,并发迭代器
性能等。 Moka v0.12 Mini Moka v0.10 Quick Cache v0.3
与并发哈希表相比,开销较小
不使用后台线程 ❌ → ✅ 已从 v0.12 版本中移除
依赖树较小

生产中的 Moka

Moka 正在为生产服务和嵌入式 Linux 设备(如家用路由器)提供动力。以下是亮点

  • crates.io:官方crate注册已经使用 Moka 在其API服务中,以减少 PostgreSQL 的负载。Moka 维护 缓存命中率约为85% 的高流量下载端点。(Moka 使用:2021年11月至今)
  • aliyundrive-webdav:这可能已经部署在数百个家用Wi-Fi路由器中,包括32位MIPS或基于ARMv5TE的SoC的低成本型号。Moka 用于缓存远程文件的元数据。(Moka 使用:2021年8月至今)

最近更改

注意 v0.12.0 在API和内部行为上进行了重大破坏性更改。请阅读 MIGRATION-GUIDE.md 了解详细信息。

目录

支持的平台

如果Rust std 库带有线程支持,则Moka 应该能在大多数64位和32位平台上运行。但是,WebAssembly(Wasm)和WASI目标不受支持。

以下平台已在CI上测试

  • Linux 64位(x86_64,arm aarch64)
  • Linux 32位(i646,armv7,armv5,mips)
    • 如果您在32位平台上遇到编译错误,请参阅 故障排除

以下平台未在CI上测试但应工作

  • macOS(arm64)
  • Windows(x86_64 msvc 和 gnu)
  • iOS(arm64)

以下平台不支持

  • WebAssembly(Wasm)和WASI目标不受支持。(见 此项目任务
  • nostd 环境(没有 std 库的平台)不受支持。
  • 16位平台不受支持。

用法

要将 Moka 添加到依赖项,运行以下命令

# To use the synchronous cache:
cargo add moka --features sync

# To use the asynchronous cache:
cargo add moka --features future

如果您想在 tokioasync-std 等异步运行时下使用缓存,您应指定 future 功能。否则,指定 sync 功能。

示例:同步缓存

线程安全的同步缓存在 sync 模块中定义。

使用 insertget_with 方法手动添加缓存条目,并在缓存中存储,直到被驱逐或手动失效。

以下是使用多线程读取和更新缓存的示例

// Use the synchronous cache.
use moka::sync::Cache;

use std::thread;

fn value(n: usize) -> String {
    format!("value {n}")
}

fn main() {
    const NUM_THREADS: usize = 16;
    const NUM_KEYS_PER_THREAD: usize = 64;

    // Create a cache that can store up to 10,000 entries.
    let cache = Cache::new(10_000);

    // Spawn threads and read and update the cache simultaneously.
    let threads: Vec<_> = (0..NUM_THREADS)
        .map(|i| {
            // To share the same cache across the threads, clone it.
            // This is a cheap operation.
            let my_cache = cache.clone();
            let start = i * NUM_KEYS_PER_THREAD;
            let end = (i + 1) * NUM_KEYS_PER_THREAD;

            thread::spawn(move || {
                // Insert 64 entries. (NUM_KEYS_PER_THREAD = 64)
                for key in start..end {
                    my_cache.insert(key, value(key));
                    // get() returns Option<String>, a clone of the stored value.
                    assert_eq!(my_cache.get(&key), Some(value(key)));
                }

                // Invalidate every 4 element of the inserted entries.
                for key in (start..end).step_by(4) {
                    my_cache.invalidate(&key);
                }
            })
        })
        .collect();

    // Wait for all threads to complete.
    threads.into_iter().for_each(|t| t.join().expect("Failed"));

    // Verify the result.
    for key in 0..(NUM_THREADS * NUM_KEYS_PER_THREAD) {
        if key % 4 == 0 {
            assert_eq!(cache.get(&key), None);
        } else {
            assert_eq!(cache.get(&key), Some(value(key)));
        }
    }
}

您可以通过克隆存储库并运行以下cargo命令来尝试同步示例:

$ cargo run --example sync_example

如果您想在键不存在时原子性地初始化和插入值,您可能需要查看文档以获取其他插入方法get_withtry_get_with

示例:异步缓存

异步(futures aware)缓存定义在future模块中。它与异步运行时环境(如Tokioasync-stdactix-rt)一起工作。要使用异步缓存,请启用名为"future"的crate功能

缓存条目通过插入方法手动添加,并存储在缓存中,直到被驱逐或手动失效。

  • 在异步上下文(async fnasync块)内部,使用insertinvalidate方法来更新缓存,并使用await它们。
  • 在任何异步上下文之外,使用blocking方法来访问insertinvalidate方法的阻塞版本。

这是一个与上一个示例类似的程序,但使用Tokio运行时异步缓存。

// Cargo.toml
//
// [dependencies]
// moka = { version = "0.12", features = ["future"] }
// tokio = { version = "1", features = ["rt-multi-thread", "macros" ] }
// futures-util = "0.3"

// Use the asynchronous cache.
use moka::future::Cache;

#[tokio::main]
async fn main() {
    const NUM_TASKS: usize = 16;
    const NUM_KEYS_PER_TASK: usize = 64;

    fn value(n: usize) -> String {
        format!("value {n}")
    }

    // Create a cache that can store up to 10,000 entries.
    let cache = Cache::new(10_000);

    // Spawn async tasks and write to and read from the cache.
    let tasks: Vec<_> = (0..NUM_TASKS)
        .map(|i| {
            // To share the same cache across the async tasks, clone it.
            // This is a cheap operation.
            let my_cache = cache.clone();
            let start = i * NUM_KEYS_PER_TASK;
            let end = (i + 1) * NUM_KEYS_PER_TASK;

            tokio::spawn(async move {
                // Insert 64 entries. (NUM_KEYS_PER_TASK = 64)
                for key in start..end {
                    // insert() is an async method, so await it.
                    my_cache.insert(key, value(key)).await;
                    // get() returns Option<String>, a clone of the stored value.
                    assert_eq!(my_cache.get(&key).await, Some(value(key)));
                }

                // Invalidate every 4 element of the inserted entries.
                for key in (start..end).step_by(4) {
                    // invalidate() is an async method, so await it.
                    my_cache.invalidate(&key).await;
                }
            })
        })
        .collect();

    // Wait for all tasks to complete.
    futures_util::future::join_all(tasks).await;

    // Verify the result.
    for key in 0..(NUM_TASKS * NUM_KEYS_PER_TASK) {
        if key % 4 == 0 {
            assert_eq!(cache.get(&key).await, None);
        } else {
            assert_eq!(cache.get(&key).await, Some(value(key)));
        }
    }
}

您可以通过克隆存储库并运行以下cargo命令来尝试异步示例:

$ cargo run --example async_example --features future

如果您想在键不存在时原子性地初始化和插入值,您可能需要查看文档以获取其他插入方法get_withtry_get_with

避免在 get 处克隆值

对于并发缓存(syncfuture缓存),get方法的返回类型是Option<V>,而不是Option<&V>,其中V是值类型。每次对现有键调用get时,它都会创建存储值的克隆V并将其返回。这是因为在任何给定时间,任何其他线程都可以更改或替换存储在缓存中的值。因此,get不能返回引用&V,因为这无法保证值会存活于引用。

如果您要存储复制成本高昂的值,请在存储到缓存之前,使用std::sync::Arc对其进行包装。Arc是一个线程安全的引用计数指针,其clone方法成本低廉。

use std::sync::Arc;

let key = ...
let large_value = vec![0u8; 2 * 1024 * 1024]; // 2 MiB

// When insert, wrap the large_value by Arc.
cache.insert(key.clone(), Arc::new(large_value));

// get() will call Arc::clone() on the stored value, which is cheap.
cache.get(&key);

示例:大小感知驱逐

如果不同的缓存条目有不同的“权重”(例如,每个条目有不同的内存占用),您可以在创建缓存时指定一个weigher闭包。闭包应返回条目的加权大小(相对大小),类型为u32,并且当总加权大小超过其max_capacity时,缓存将驱逐条目。

use moka::sync::Cache;

fn main() {
    let cache = Cache::builder()
        // A weigher closure takes &K and &V and returns a u32 representing the
        // relative size of the entry. Here, we use the byte length of the value
        // String as the size.
        .weigher(|_key, value: &String| -> u32 {
            value.len().try_into().unwrap_or(u32::MAX)
        })
        // This cache will hold up to 32MiB of values.
        .max_capacity(32 * 1024 * 1024)
        .build();
    cache.insert(0, "zero".to_string());
}

请注意,在做出驱逐选择时,不使用加权大小。

您可以尝试通过克隆仓库并运行以下cargo命令来尝试大小感知淘汰示例:

$ cargo run --example size_aware_eviction

过期策略

Moka支持以下过期策略

  • 缓存级别的过期策略
    • 缓存级别的策略应用于缓存中的所有条目。
    • 存活时间(TTL):缓存条目将在从insert插入后指定的持续时间过期。
    • 空闲时间(TTI):缓存条目将在从getinsert指定的持续时间过期。
  • 每条目过期策略
    • 每条目过期策略允许您为每个条目设置不同的过期时间。

有关上述策略的详细信息及示例,请参阅文档中的“示例:基于时间的过期”部分(sync::Cachefuture::Cache)。

最低支持的Rust版本

Moka支持的最低Rust版本(MSRV)如下

功能 MSRV
默认功能 Rust 1.65.0(2022年11月3日)
future Rust 1.65.0(2022年11月3日)

它将保持至少6个月的滚动MSRV策略。如果仅启用默认功能,MSRV将保守更新。当使用其他功能,如future时,MSRV可能会更频繁地更新,直到最新的稳定版。在两种情况下,增加MSRV都不被视为semver破坏性更改。

故障排除

某些32位平台上的编译错误

在以下一些32位目标平台(包括)上,您可能会遇到编译错误

  • armv5te-unknown-linux-musleabi
  • mips-unknown-linux-musl
  • mipsel-unknown-linux-musl
error[E0432]: unresolved import `std::sync::atomic::AtomicU64`
  --> ... /moka-0.5.3/src/sync.rs:10:30
   |
10 |         atomic::{AtomicBool, AtomicU64, Ordering},
   |                              ^^^^^^^^^
   |                              |
   |                              no `AtomicU64` in `sync::atomic`

这些错误可能发生,因为这些平台没有提供std::sync::atomic::AtomicU64,而Moka使用它。

您可以通过禁用Moka的默认功能之一atomic64来解决这些错误。编辑您的Cargo.toml,在依赖声明中添加default-features = false

[dependencies]
moka = { version = "0.12", default-features = false, features = ["sync"] }
# Or
moka = { version = "0.12", default-features = false, features = ["future"] }

这将使Moka切换到后备实现,因此它可以编译。

开发 Moka

运行所有测试

要运行所有测试,包括future功能和README上的文档测试,请使用以下命令

$ RUSTFLAGS='--cfg trybuild' cargo test --all-features

运行不带默认功能的所有测试

$ RUSTFLAGS='--cfg trybuild' cargo test \
    --no-default-features --features 'future, sync'

生成文档

$ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs"' \
    doc --no-deps --features 'future, sync'

路线图

有关最新和详细计划的详细信息,请参阅项目路线图

但这里有一些亮点

  • 大小感知淘汰。(通过#24的v0.7.0)
  • API稳定化。(较小的核心API,常用方法的更短名称)(通过#105的v0.8.0)
    • 例如。
    • get_or_insert_with(K, F)get_with(K, F)
    • get_or_try_insert_with(K, F)try_get_with(K, F)
    • time_to_live()policy().time_to_live()
  • 驱逐通知。(v0.9.0 通过 #145
  • 使用分层定时轮进行(每个条目)变量过期。(v0.11.0 通过 #248
  • 移除后台线程。(v0.12.0 通过 #294#316
  • 添加upsert和compute方法。(v0.12.3 通过 #370
  • 缓存统计信息(命中率等)。(详情
  • 将TinyLFU升级为Window-TinyLFU。(详情
  • 从快照中恢复缓存。(详情

关于名称

Moka的名字来源于摩卡壶,一种利用蒸汽压力煮沸水来煮类似浓缩咖啡的家用咖啡壶。

这个名字意味着以下事实和希望

  • Moka是Java Caffeine缓存家族的一部分。
  • 它是用Rust编写的。(许多摩卡壶是由铝合金或不锈钢制成。我们知道它们不会生锈。)
  • 它应该很快。(“Espresso”在意大利语中意为快速)
  • 它应该像摩卡壶一样容易使用。

致谢

咖啡因

Moka的架构深受Java库Caffeine的启发。感谢Ben Manes和Caffeine的所有贡献者。

cht

位于moka::cht模块下的并发哈希表源文件是从cht crate v0.4.1复制的,并经过我们修改。我们这样做是为了更好地集成。cht v0.4.1及更早版本遵循MIT许可。

感谢Gregory Meyer。

许可

Moka根据您的选择,采用以下任一许可进行分发

  • MIT许可
  • Apache License(版本2.0)

有关详细信息,请参阅LICENSE-MITLICENSE-APACHE

依赖项

~2.2–9.5MB
~87K SLoC