aster_forge_cache
aster_forge_cache 提供共享缓存抽象和内存/Redis 后端构造。公共 API 是 byte-oriented 的 object-safe trait,并通过扩展 trait 提供 JSON 便利方法。
适用场景
- 产品需要一个
Arc<dyn CacheBackend>。 - 本地开发或 Redis 不可用时回退到内存缓存。
- 多个服务复用同样的 Redis 健康检查和 fallback 逻辑。
- 需要原子
take、set if absent或 prefix invalidation。 - 需要 Bloom filter 为 cache-aside 查询提供低成本的存在性预筛选。
不适合放在这里的内容:
- 产品缓存 key 命名规范。
- 缓存失效的业务策略。
- Session、token、验证码等产品语义。
Cargo feature
默认 feature:
memory
可选 feature:
bloom:启用并发 Bloom filter 和原子流式重建 session。redis:启用 Redis backend。Redis backend 内部使用 memory fallback,所以会自动启用memory。runtime-component:启用cache_health_component(...)等 runtime health 组件。
只使用内存缓存时:
aster_forge_cache = { git = "https://github.com/AsterCommunity/AsterForge", default-features = false, features = ["memory"] }使用 Redis 和标准 health component 时:
aster_forge_cache = { git = "https://github.com/AsterCommunity/AsterForge", default-features = false, features = ["redis", "runtime-component"] }同时使用 Bloom 和内存/Redis backend 时:
aster_forge_cache = { git = "https://github.com/AsterCommunity/AsterForge", default-features = false, features = ["bloom", "redis"] }配置结构
CacheConfig 可以直接嵌进产品启动配置结构:
#[derive(serde::Deserialize)]
struct Config {
#[serde(default)]
cache: aster_forge_cache::CacheConfig,
}let config = aster_forge_cache::CacheConfig {
backend: "redis".to_string(),
endpoint: "redis://127.0.0.1/".to_string(),
default_ttl: 3600,
};
let cache = aster_forge_cache::create_cache(&config).await;默认配置使用 memory backend、空 endpoint 和 3600 秒 TTL,和 Aster 产品配置文件里的历史默认值保持一致。
配置文件里应该使用 endpoint。为了不破坏已有部署,CacheConfig 反序列化时仍接受历史键 redis_url 作为 alias;Rust API 不保留 redis_url 字段。
create_cache() 返回 Arc<dyn CacheBackend>。Redis 初始化失败时会记录 warn 并回退到 memory backend。
CacheBackend 边界
核心 trait:
get_bytestake_bytesset_bytesset_bytes_if_absentdeletedelete_manyinvalidate_prefixhealth_check
CacheExt 提供 JSON 方法:
get<T>set<T>take<T>
JSON 反序列化失败时返回 None,不会抛出产品错误。对关键业务状态不要只靠这个静默行为,产品侧应该有兜底或重建逻辑。
invalidate_prefix(prefix) 的前缀按字面语义处理。Redis backend 用 SCAN MATCH(glob 语法)实现,内部会先转义前缀里的 \、*、?、[、] 再拼通配符——产品侧可以安全地把含用户可控部分的 key 前缀传进来,不会因为 glob 元字符误删非预期 key 或漏删目标 key。
TTL 契约
ttl_secs: None使用 backend 构造时的默认 TTL。ttl_secs: Some(0)表示立即过期。set_bytes等价于删除该 key(Redis 会拒绝SETEX 0,backend 统一归一化为这个语义而不是发出非法命令);set_bytes_if_absent只报告 key 当前是否有存活值,无论结果如何都不会留下值。- per-entry TTL 是精确的:memory backend 通过 moka 的 per-entry
Expiry以每个值的绝对过期时间判定,比默认 TTL 更长的条目不会被提前驱逐,default_ttl = 0也不会让整个缓存变成只写不存——与 Redis SETEX 的语义一致。 - insert-if-absent 的本地 reservation 与被保护值同寿命:值过期后 key 立即可被重新 reserve 插入(零 TTL 值的 reservation 也随之立即失效,因为谁"赢得"一个立即过期的值本就不可观测)。reservation 由 RAII guard 持有:reserve 后输给并发插入的调用方在 drop 时自动释放 reservation,不会在值被 TTL 驱逐后假性阻塞后续插入。
- memory 和 Redis backend 对这些边界的行为一致,单元测试和真实 Redis 容器测试共同锁定契约。
Redis fallback
Redis backend 有健康检查和 fallback circuit。fallback circuit 只对可用性故障打开:连接 IO 错误、cluster 连接缺失,以及 LOADING/TRYAGAIN/CLUSTERDOWN/MASTERDOWN/READONLY 等瞬时服务端错误。确定性命令错误(如 WRONGTYPE、非法参数)只会记录 warn 并对单次操作走 fallback,不会把整个 backend 切到降级状态。
故障期写入只落在本地影子副本里。任何成功的 Redis 操作(get/set/take/set_if_absent)都会清除该 key 的本地影子:Redis 恢复权威地位后,影子不是冗余就是陈旧,留着它会在下一次故障时复活从未持久化的数据。
产品侧应该决定:
- 是否使用 Forge 标准 health check 暴露 Redis fallback。
- fallback 期间是否允许登录、验证码、任务调度等功能继续运行。
- 是否把 fallback 状态暴露到 metrics 或 admin overview。
Forge 只负责后端机制,不负责产品可用性策略。
Bloom filter
bloom feature 提供:
aster_forge_cache::bloom::BloomConfigaster_forge_cache::bloom::BloomFilteraster_forge_cache::bloom::BloomRebuildaster_forge_cache::bloom::BloomError
产品直接组合这些 primitive,并保留自己的查询顺序、key 规范、TTL、持久化来源和 false-positive 处理:
use std::sync::Arc;
use aster_forge_cache::bloom::{BloomConfig, BloomFilter};
let filter = Arc::new(BloomFilter::new(BloomConfig::new(10_000, 0.001))?);
filter.insert("short-code");
if filter.contains("short-code") {
// Continue with the product-owned negative/object/storage lookup chain.
}大数据集重建使用 start_rebuild() 创建 session,逐批调用 insert_many(),数据源完整结束后再 commit()。commit 前读取端继续使用旧 filter;重建期间的并发 insert() 会被记录并合并到新 filter。数据源报错时直接丢弃 session,旧 filter 保持可用。
同一 filter 只允许一个 active rebuild。重建期间再次调用 start_rebuild() 或 clear() 会返回 BloomError::RebuildInProgress,避免清空并发写入 buffer。
健康检查
需要 Cargo feature:runtime-component。
如果产品使用 aster_forge_runtime::RuntimeComponentRegistry,可以直接注册标准 cache diagnostics:
registry.register_bundle(aster_forge_cache::cache_health_component(
config.cache.clone(),
cache.clone(),
));这个检查注册在 cache component 下,只进入 diagnostics scope,并且是 optional health check。行为:
- 配置 backend 和 active backend 不一致时返回 degraded,例如 Redis 初始化失败后回退 memory。
- active backend
health_check()成功时返回 healthy。 - active backend
health_check()失败时返回 unhealthy。 - report detail 会包含
active_backend,fallback 时还会包含configured_backend。
产品仍然决定这个 diagnostics 结果是否影响 readiness、admin overview 或告警策略。普通产品不应该再重复写 cache backend fallback/ping 的 report 拼装。
新产品接入时优先使用 cache_health_component(...),这样 health 也保持 component 化。低层 registry 注册函数是 crate 内部实现细节,不作为子系统 API 暴露。
测试要求
- memory backend 的 TTL、take、set-if-absent。
- Redis backend 可用时的读写和 prefix invalidation。
- Redis 不可用时回退 memory。
- TTL 边界:
Some(0)立即过期、1 秒 TTL 在服务端真实过期。 - Redis 错误分类:WRONGTYPE 等命令错误不触发 fallback circuit。
- 产品关键缓存 key 的命名和失效策略。
真实 Redis 集成测试使用 aster_forge_test 的共享容器(tests/redis_container.rs),运行:
cargo test -p aster_forge_cache --features redis共享容器在多次运行之间保留数据,测试 key 必须带进程唯一前缀,crate 内的 unique_key(...) helper 已处理。
primitive benchmark 位于 crates/aster_forge_cache/benches/cache.rs,覆盖 Bloom check/insert/bulk insert 和 memory backend get/set/delete。只编译 benchmark:
cargo bench -p aster_forge_cache --features bloom,memory --no-run参考项目
- AsterDrive:适合参考复杂服务如何集中创建 cache,并在健康检查里展示 backend 状态。
- AsterYggdrasil:适合参考轻量服务如何只依赖
CacheBackendtrait,避免业务层绑定 Redis。