Project Icon

moka

Rust高性能并发缓存库 优化的缓存替换算法

Moka是一个快速、并发的Rust缓存库,基于哈希表实现。它支持高并发检索和更新,采用先进的缓存替换算法实现容量限制。Moka提供同步和异步缓存、基于数量或权重的边界、过期策略和驱逐监听等功能,适用于需要高性能缓存的Rust项目。

Moka

[![GitHub Actions][gh-actions-badge]][gh-actions] [![crates.io 发布][release-badge]][crate] [![文档][docs-badge]][docs] [![依赖状态][deps-rs-badge]][deps-rs] [![codecov][codecov-badge]][codecov] ![许可证][license-badge] FOSSA 状态

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


Moka 是一个快速的、并发的 Rust 缓存库。Moka 的灵感来自于 Java 的 [Caffeine][caffeine-git] 库。

Moka 在哈希映射之上提供缓存实现。它们支持完全并发的检索操作,并且对更新操作具有高度的预期并发性。

所有缓存都尽最大努力通过使用条目替换算法来限制哈希映射的大小,以确定在超出容量时要驱逐哪些条目。

特性

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

  • 线程安全、高度并发的内存缓存实现:
    • 可以在操作系统线程间共享的同步缓存。
    • 异步(支持 futures)缓存。
  • 缓存可以通过以下方式之一进行限制:
    • 最大条目数。
    • 条目的总加权大小。(大小感知驱逐)
  • 通过使用受 Caffeine 启发的条目替换算法来维持接近最优的命中率:
    • 缓存的准入由最不经常使用(LFU)策略控制。
    • 缓存的驱逐由最近最少使用(LRU)策略控制。
    • [这里提供了更多详细信息和一些基准测试结果][tiny-lfu]。
  • 支持过期策略:
    • 生存时间。
    • 空闲时间。
    • 每个条目的可变过期时间。
  • 支持驱逐监听器,当条目从缓存中移除时将调用回调函数。

为您的用例选择正确的缓存

没有一种缓存实现能够完美适用于所有用例。Moka 是一个复杂的软件,可能对您的用例来说过于复杂。有时更简单的缓存,如 [Mini Moka][mini-moka-crate] 或 [Quick Cache][quick-cache] 可能更适合。

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

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

Moka 在生产环境中的应用

Moka 正在为生产服务以及家用路由器等嵌入式 Linux 设备提供支持。以下是一些亮点:

  • crates.io:官方 crate 注册中心在其 API 服务中使用 Moka 来减少 PostgreSQL 的负载。Moka 为高流量的下载端点维持了[约 85% 的缓存命中率][gh-discussions-51]。 (Moka 使用时间:2021 年 11 月至今)
  • [aliyundrive-webdav][aliyundrive-webdav-git]:这个云盘的 WebDAV 网关可能已部署在数百台家用 Wi-Fi 路由器中,包括使用 32 位 MIPS 或基于 ARMv5TE 的 SoC 的廉价型号。Moka 用于缓存远程文件的元数据。 (Moka 使用时间:2021 年 8 月至今) [gh-discussions-51]: https://github.com/moka-rs/moka/discussions/51 [aliyundrive-webdav-git]: https://github.com/messense/aliyundrive-webdav

最近的变更

注意 v0.12.0 版本对API和内部行为进行了重大破坏性更改。请阅读 MIGRATION-GUIDE.md 了解详情。

目录

支持的平台

Moka应该可以在大多数64位和32位平台上运行,只要该平台支持Rust std库和线程功能。然而,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添加到您的依赖项中,请运行以下cargo add命令:

# 使用同步缓存:
cargo add moka --features sync

# 使用异步缓存:
cargo add moka --features future

如果您想在异步运行时(如tokioasync-std)下使用缓存,应该指定future特性。否则,请指定sync特性。

示例:同步缓存

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

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

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

// 使用同步缓存。
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;

    // 创建一个可以存储多达10,000个条目的缓存。
    let cache = Cache::new(10_000);

    // 生成线程并同时读取和更新缓存。
    let threads: Vec<_> = (0..NUM_THREADS)
        .map(|i| {
            // 要在线程间共享相同的缓存,克隆它。
            // 这是一个开销很小的操作。
            let my_cache = cache.clone();
            let start = i * NUM_KEYS_PER_THREAD;
            let end = (i + 1) * NUM_KEYS_PER_THREAD;

            thread::spawn(move || {
                // 插入64个条目。(NUM_KEYS_PER_THREAD = 64)
                for key in start..end {
                    my_cache.insert(key, value(key));
                    // get()返回Option<String>,是存储值的克隆。
                    assert_eq!(my_cache.get(&key), Some(value(key)));
                }

                // 使插入条目中的每第4个元素失效。
                for key in (start..end).step_by(4) {
                    my_cache.invalidate(&key);
                }
            })
        })
        .collect();

    // 等待所有线程完成。
    threads.into_iter().for_each(|t| t.join().expect("Failed"));

    // 验证结果。
    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)缓存定义在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 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}")
    }

    // 创建一个可以存储多达 10,000 个条目的缓存。
    let cache = Cache::new(10_000);

    // 生成异步任务并对缓存进行写入和读取。
    let tasks: Vec<_> = (0..NUM_TASKS)
        .map(|i| {
            // 要在异步任务之间共享相同的缓存,请克隆它。
            // 这是一个开销很小的操作。
            let my_cache = cache.clone();
            let start = i * NUM_KEYS_PER_TASK;
            let end = (i + 1) * NUM_KEYS_PER_TASK;

            tokio::spawn(async move {
                // 插入 64 个条目。(NUM_KEYS_PER_TASK = 64)
                for key in start..end {
                    // insert() 是一个异步方法,所以要 await 它。
                    my_cache.insert(key, value(key)).await;
                    // get() 返回 Option<String>,是存储值的克隆。
                    assert_eq!(my_cache.get(&key).await, Some(value(key)));
                }

                // 使插入条目中的每第 4 个元素失效。
                for key in (start..end).step_by(4) {
                    // invalidate() 是一个异步方法,所以要 await 它。
                    my_cache.invalidate(&key).await;
                }
            })
        })
        .collect();

    // 等待所有任务完成。
    futures_util::future::join_all(tasks).await;

    // 验证结果。
    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 的克隆并返回。这是因为 Cache 允许来自线程的并发更新,所以存储在缓存中的值可以随时被任何其他线程删除或替换。get 无法返回引用 &V,因为不可能保证该值的生命周期长于引用。

如果你想存储克隆成本高昂的值,在存储到缓存之前用 std::sync::Arc 包装它们。Arc 是一个线程安全的引用计数指针,其 clone() 方法开销很小。

use std::sync::Arc;

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

// 插入时,用 Arc 包装 large_value。
cache.insert(key.clone(), Arc::new(large_value));

// get() 将对存储的值调用 Arc::clone(),这个操作开销很小。
cache.get(&key);

示例:大小感知的驱逐

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

use moka::sync::Cache;

fn main() {
    let cache = Cache::builder()
        // weigher 闭包接受 &K 和 &V,并返回一个表示条目相对大小的 u32。
        // 这里,我们使用值 String 的字节长度作为大小。
        .weigher(|_key, value: &String| -> u32 {
            value.len().try_into().unwrap_or(u32::MAX)
        })
        // 这个缓存将最多保存 32MiB 的值。
        .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日)
futureRust 1.65.0 (2022年11月3日)

它将保持至少 6 个月的滚动 MSRV 政策。如果只启用默认功能,MSRV 将会保守地更新。当使用其他功能(如 future)时,MSRV 可能会更频繁地更新,最高可达最新的稳定版。在这两种情况下,增加 MSRV 都不被视为破坏语义版本的变更。

故障排除

在某些 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 使用了它。

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

[dependencies]
moka = { version = "0.12", default-features = false, features = ["sync"] }
# 或
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'

路线图

查看项目路线图以获取最新和详细的计划。

以下是一些亮点:

  • 大小感知的淘汰。(v0.7.0 通过 #24
  • API 稳定化。(更小的核心 API,常用方法名称更短)(v0.8.0 通过 #105
    • 例如:
    • 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
  • 添加更新和计算方法。(v0.12.3 通过 #370
  • 缓存统计(命中率等)。(详情
  • 将 TinyLFU 升级为 Window-TinyLFU。([详情][tiny-lfu])
  • 从快照恢复缓存。(详情

关于名称

Moka 的名字来源于 摩卡壶,一种用蒸汽压力将沸水压入咖啡粉中来冲泡浓缩咖啡的炉灶咖啡壶。

这个名字暗示了以下事实和期望:

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

致谢

Caffeine

Moka 的架构很大程度上受到 Java 的 [Caffeine][caffeine-git] 库的启发。感谢 Ben Manes 和 Caffeine 的所有贡献者。

cht

moka::cht 模块下的并发哈希表源文件复制自 cht crate v0.4.1 并经我们修改。我们这样做是为了更好地集成。cht v0.4.1 及更早版本在 MIT 许可证下授权。

感谢 Gregory Meyer。

许可证

Moka 根据以下两种许可之一分发:

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

由你选择。

详情请参见 LICENSE-MITLICENSE-APACHE

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号