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]
注意
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.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 注册中心在其 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在生产环境中的应用
- 变更日志
- 支持的平台
- 使用方法
- 示例(第1部分)
- 避免在
get
时克隆值 - 示例(第2部分)
- 过期策略
- 最低支持的Rust版本
- 故障排除
- 开发Moka
- 路线图
- 关于名称
- 致谢
- 许可证
支持的平台
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
如果您想在异步运行时(如tokio
或async-std
)下使用缓存,应该指定future
特性。否则,请指定sync
特性。
示例:同步缓存
线程安全的同步缓存定义在sync
模块中。
缓存条目通过insert
或get_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_with
和try_get_with
。
示例:异步缓存
异步(支持futures)缓存定义在future
模块中。
它可以与异步运行时(如Tokio、
async-std或actix-rt)一起使用。
要使用异步缓存,请启用名为"future"的crate特性。
缓存条目通过插入方法手动添加,并存储在缓存中,直到被驱逐或手动失效:
- 在异步上下文(
async fn
或async
块)中,使用insert
或invalidate
方法更新缓存并await
它们。 - 在任何异步上下文之外,使用
blocking
方法来访问insert
或invalidate
方法的阻塞版本。
以下是一个与前面示例类似的程序,但使用了异步缓存和 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_with
和 try_get_with
。
避免在 get
时克隆值
对于并发缓存(sync
和 future
缓存),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):缓存条目将在从
get
或insert
开始经过指定持续时间后过期。
- 每条目过期策略:
- 每条目过期允许你为每个条目设置不同的过期时间。
有关上述策略的详细信息和示例,请参阅文档中的"示例:基于时间的过期"部分(sync::Cache
,future::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 都不被视为破坏语义版本的变更。
故障排除
在某些 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-MIT 和 LICENSE-APACHE。