rs-zero 0.2.6

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
# Cache 手册

`rs-zero` 的缓存能力分为底层 store、L1/L2 组合和高层 cache-aside 策略。底层 store 负责读写,cache-aside 负责防击穿、防穿透、TTL 抖动和统计。

## Feature

```toml
rs-zero = { version = "0.2.1", features = ["cache"] }
# 需要真实 Redis 或 Redis 分片时:
rs-zero = { version = "0.2.1", features = ["cache", "cache-redis"] }
```

## Memory store

`MemoryCacheStore` 适合单元测试和本地开发。它支持 JSON helper 和 TTL,但不提供容量上限、LRU 淘汰或跨进程共享能力。

```rust
use rs_zero::cache::{CacheKey, CacheStore, MemoryCacheStore};

let store = MemoryCacheStore::new();
let key = CacheKey::new("app", ["user", "1"]);
store.set_json(&key, &serde_json::json!({"name":"Ada"}), None).await?;
```

## L1 LRU store

`LruCacheStore` 是有界本进程 L1 cache。它实现 `CacheStore`,支持容量上限、TTL、命中刷新 recency、删除和快照统计。它不替代 Redis,也不提供跨实例失效广播。

```rust
use rs_zero::cache::{CacheKey, CacheStore, LruCacheStore};

let l1 = LruCacheStore::new(10_000)?;
let key = CacheKey::new("app", ["user", "42"]);
l1.set_json(&key, &serde_json::json!({"id":42}), None).await?;
let snapshot = l1.snapshot().await;
assert!(snapshot.entries <= snapshot.capacity);
```

## Two-level cache

`TwoLevelCacheStore<L1, L2>` 将 L1 与权威 L2 组合成一个 `CacheStore`:读路径先查 L1,miss 后查 L2 并回填 L1;写入先写 L2,成功后写 L1;删除会同时删除两层并返回可诊断错误。

```rust
use rs_zero::cache::{LruCacheStore, TwoLevelCacheStore};
use rs_zero::cache::redis::{RedisCacheConfig, RedisCacheStore};

let l1 = LruCacheStore::new(10_000)?;
let l2 = RedisCacheStore::new(RedisCacheConfig::default())?;
let store = TwoLevelCacheStore::new(l1, l2);
```

## Redis store

启用 `cache-redis` 后,`RedisCacheStore` 会使用真实 Redis client 执行 `GET`、`SET`、`SETEX`、`DEL` 和 `PING` 健康检查。默认测试不连接 Redis;真实 Redis 测试使用 ignored/external 路径。

```rust
use rs_zero::cache::{CacheKey, CacheStore};
use rs_zero::cache::redis::{RedisCacheConfig, RedisCacheStore};

let store = RedisCacheStore::new(RedisCacheConfig::default())?;
store.health_check().await?;
let key = CacheKey::new(store.namespace(), ["user", "1"]);
store.set_json(&key, &serde_json::json!({"id":1}), None).await?;
```

删除失败可以开启有界后台重试。调用方仍会收到本次删除错误,后台 worker 会继续尝试清理,避免写后删缓存失败被完全丢失:

```rust
use std::time::Duration;
use rs_zero::cache::redis::{RedisCacheConfig, RedisDeleteRetryConfig, RedisCacheStore};

let store = RedisCacheStore::new(RedisCacheConfig {
    delete_retry: RedisDeleteRetryConfig {
        enabled: true,
        capacity: 1024,
        max_attempts: 3,
        initial_delay: Duration::from_millis(50),
    },
    ..RedisCacheConfig::default()
})?;
```

## Redis Cluster 与 application-level sharding

`RedisCacheStore` 在 `RedisCacheConfig.cluster.enabled = true` 时使用 `redis-rs` 原生 Cluster client。Cluster 模式下 `url` 是逗号分隔的 startup node URL 列表,真实 slot 路由、`MOVED` / `ASK`、拓扑刷新和重试由底层成熟客户端处理。

```rust
use rs_zero::cache::redis::{RedisCacheConfig, RedisCacheStore};

let store = RedisCacheStore::new(RedisCacheConfig {
    url: "redis://10.0.0.10:7000,redis://10.0.0.11:7001".to_string(),
    cluster: rs_zero::cache::redis::RedisClusterConfig {
        enabled: true,
        max_redirects: 8,
        read_from_replicas: false,
    },
    ..RedisCacheConfig::default()
})?;
```

`RedisShardedCacheStore` 支持多个 Redis 节点和权重,按完整 cache key 使用加权 rendezvous hashing 选择节点。它是应用层分片,不是 Redis Cluster 协议 adapter。节点列表变化会导致部分 key 冷启动。

```rust
use rs_zero::cache::redis::{
    RedisNodeConfig, RedisShardedCacheConfig, RedisShardedCacheStore,
};

let store = RedisShardedCacheStore::new(RedisShardedCacheConfig {
    namespace: "user-service".to_string(),
    nodes: vec![
        RedisNodeConfig::new("redis-a", "redis://10.0.0.10:6379"),
        RedisNodeConfig::new("redis-b", "redis://10.0.0.11:6379").with_weight(2),
    ],
    ..RedisShardedCacheConfig::default()
})?;
store.health_check().await?;
```

## Cache-aside

`CacheAside` 在 cache miss 时合并同 key 并发 loader,避免击穿;loader 返回 `None` 时写入短 TTL 负缓存,避免穿透;正负缓存 TTL 都会应用 per-key jitter,降低同批 key 同时过期的概率。

如果缓存中的 JSON 无法反序列化,`CacheAside` 会把该值视为坏缓存:先尝试删除缓存,再回源重新加载。删除失败会记录为 cache delete error,但不会把坏缓存直接返回给业务。

```rust
use rs_zero::cache::{CacheAside, CacheAsideConfig, CacheKey, LruCacheStore};

let cache = CacheAside::new(LruCacheStore::new(1024)?, CacheAsideConfig::default());
let key = CacheKey::new("app", ["user", "42"]);
let value: Option<serde_json::Value> = cache
    .get_or_load_json(&key, || async {
        Ok(Some(serde_json::json!({"id":42})))
    })
    .await?;
```

## 生产注意事项

- namespace 必须稳定,避免不同服务或环境共享同一 key 空间。
- Redis URL、timeout、TTL 应来自配置,不要写死在 handler 中。
- Two-level cache 以 L2 为权威;L2 写失败时不会写 L1。
- L1 是进程内缓存,服务滚动发布、扩缩容和多实例写入都可能造成短期不一致,写路径必须主动失效相关 key。
- Redis 分片是应用层分片;调整节点列表或权重会造成部分 key 迁移和缓存冷启动。
- Redis 分片批量删除会按 shard 聚合执行;部分 shard 失败时返回聚合错误。
- Redis Cluster 使用 `RedisCacheStore` 的 cluster 模式;应用级 Redis 分片不是 Redis Cluster slot 客户端。
- 默认 CI 不应依赖 Redis;真实 Redis 验证使用 `cargo test --features cache-redis --test cache_integration -- --ignored`- cache-aside loader 应只读取权威数据源,不应在 loader 内再次写缓存。

## 相关测试

- `cargo test -p rs-zero cache_aside`
- `cargo test --test cache_integration`
- `cargo test --no-default-features --features cache-redis --test cache_redis_sharding`
- `cargo check -p rs-zero --no-default-features --features cache-redis`