salvo-caching
1
总安装量
1
周安装量
#49657
全站排名
安装命令
npx skills add https://github.com/salvo-rs/salvo-skills --skill salvo-caching
Agent 安装分布
amp
1
opencode
1
cursor
1
kimi-cli
1
github-copilot
1
Skill 文档
Salvo Caching Strategies
This skill helps implement caching in Salvo applications for better performance.
HTTP Cache Headers
Set cache control headers to enable browser and proxy caching:
use salvo::prelude::*;
#[handler]
async fn cached_response(res: &mut Response) -> &'static str {
// Cache for 1 hour
res.headers_mut().insert(
"Cache-Control",
"public, max-age=3600".parse().unwrap()
);
"This response will be cached by browsers"
}
Cache-Control Directives
// Public caching (can be cached by proxies)
res.headers_mut().insert("Cache-Control", "public, max-age=3600".parse().unwrap());
// Private caching (only browser can cache)
res.headers_mut().insert("Cache-Control", "private, max-age=3600".parse().unwrap());
// No caching
res.headers_mut().insert("Cache-Control", "no-store".parse().unwrap());
// Stale while revalidate (serve stale while refreshing)
res.headers_mut().insert(
"Cache-Control",
"public, max-age=3600, stale-while-revalidate=86400".parse().unwrap()
);
ETag for Validation
use salvo::prelude::*;
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
#[handler]
async fn with_etag(req: &mut Request, res: &mut Response) -> Result<Json<Data>, StatusError> {
let data = get_data().await?;
// Generate ETag from content
let mut hasher = DefaultHasher::new();
format!("{:?}", data).hash(&mut hasher);
let etag = format!("\"{}\"", hasher.finish());
// Check If-None-Match header
if let Some(if_none_match) = req.header::<String>("If-None-Match") {
if if_none_match == etag {
res.status_code(StatusCode::NOT_MODIFIED);
return Err(StatusError::not_modified());
}
}
res.headers_mut().insert("ETag", etag.parse().unwrap());
Ok(Json(data))
}
In-Memory Response Cache
use salvo::prelude::*;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Clone)]
struct CacheEntry {
data: String,
expires_at: Instant,
}
#[derive(Clone)]
struct ResponseCache {
store: Arc<RwLock<HashMap<String, CacheEntry>>>,
ttl: Duration,
}
impl ResponseCache {
fn new(ttl: Duration) -> Self {
Self {
store: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
fn get(&self, key: &str) -> Option<String> {
let store = self.store.read().ok()?;
let entry = store.get(key)?;
if Instant::now() < entry.expires_at {
Some(entry.data.clone())
} else {
None
}
}
fn set(&self, key: String, data: String) {
if let Ok(mut store) = self.store.write() {
store.insert(key, CacheEntry {
data,
expires_at: Instant::now() + self.ttl,
});
}
}
}
#[handler]
async fn cache_middleware(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl
) {
let cache = depot.obtain::<ResponseCache>().unwrap();
let cache_key = format!("{}:{}", req.method(), req.uri().path());
// Try cache first
if let Some(cached) = cache.get(&cache_key) {
res.headers_mut().insert("X-Cache", "HIT".parse().unwrap());
res.render(cached);
return;
}
// Process request
ctrl.call_next(req, depot, res).await;
res.headers_mut().insert("X-Cache", "MISS".parse().unwrap());
// Cache successful responses
// Note: In production, extract body from response properly
}
Using Moka for Caching
[dependencies]
moka = { version = "0.12", features = ["future"] }
use salvo::prelude::*;
use moka::future::Cache;
use std::sync::Arc;
use std::time::Duration;
type AppCache = Cache<String, String>;
async fn create_cache() -> AppCache {
Cache::builder()
.max_capacity(10_000)
.time_to_live(Duration::from_secs(300))
.build()
}
#[handler]
async fn cached_handler(req: &mut Request, depot: &mut Depot) -> Result<String, StatusError> {
let cache = depot.obtain::<AppCache>().unwrap();
let key = req.uri().path().to_string();
// Try cache
if let Some(cached) = cache.get(&key).await {
return Ok(cached);
}
// Compute result
let result = expensive_computation().await?;
// Store in cache
cache.insert(key, result.clone()).await;
Ok(result)
}
#[tokio::main]
async fn main() {
let cache = create_cache().await;
let router = Router::new()
.hoop(affix_state::inject(cache))
.get(cached_handler);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Database Query Caching
use salvo::prelude::*;
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: i64,
name: String,
email: String,
}
type UserCache = Cache<i64, User>;
async fn create_user_cache() -> UserCache {
Cache::builder()
.max_capacity(1000)
.time_to_live(Duration::from_secs(60))
.build()
}
#[handler]
async fn get_user(req: &mut Request, depot: &mut Depot) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id").ok_or_else(|| StatusError::bad_request())?;
let cache = depot.obtain::<UserCache>().unwrap();
let pool = depot.obtain::<PgPool>().unwrap();
// Check cache
if let Some(user) = cache.get(&id).await {
return Ok(Json(user));
}
// Query database
let user = sqlx::query_as::<_, User>("SELECT id, name, email FROM users WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await
.map_err(|_| StatusError::internal_server_error())?
.ok_or_else(|| StatusError::not_found())?;
// Cache result
cache.insert(id, user.clone()).await;
Ok(Json(user))
}
Cache Invalidation
use moka::future::Cache;
struct CacheService {
user_cache: Cache<i64, User>,
}
impl CacheService {
// Invalidate single entry
async fn invalidate_user(&self, id: i64) {
self.user_cache.invalidate(&id).await;
}
// Invalidate all entries
async fn invalidate_all_users(&self) {
self.user_cache.invalidate_all();
}
// Invalidate matching entries
async fn invalidate_users_by_ids(&self, ids: &[i64]) {
for id in ids {
self.user_cache.invalidate(id).await;
}
}
}
// Invalidate on update
#[handler]
async fn update_user(
req: &mut Request,
depot: &mut Depot
) -> Result<StatusCode, StatusError> {
let id = req.param::<i64>("id").unwrap();
// Update in database...
// Invalidate cache
let cache = depot.obtain::<UserCache>().unwrap();
cache.invalidate(&id).await;
Ok(StatusCode::OK)
}
Conditional Requests
use salvo::prelude::*;
use time::OffsetDateTime;
#[handler]
async fn conditional_get(req: &mut Request, res: &mut Response) -> Result<Json<Data>, StatusError> {
let data = get_data().await?;
let last_modified = data.updated_at;
// Check If-Modified-Since
if let Some(since) = req.header::<String>("If-Modified-Since") {
// Parse and compare timestamps
// Return 304 if not modified
}
res.headers_mut().insert(
"Last-Modified",
last_modified.format(&time::format_description::well_known::Rfc2822)
.unwrap()
.parse()
.unwrap()
);
Ok(Json(data))
}
Complete Caching Example
use salvo::prelude::*;
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Serialize)]
struct Product {
id: i64,
name: String,
price: f64,
}
type ProductCache = Cache<i64, Product>;
#[handler]
async fn get_product(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<Json<Product>, StatusError> {
let id = req.param::<i64>("id").ok_or_else(|| StatusError::bad_request())?;
let cache = depot.obtain::<ProductCache>().unwrap();
// Check cache
if let Some(product) = cache.get(&id).await {
res.headers_mut().insert("X-Cache", "HIT".parse().unwrap());
res.headers_mut().insert("Cache-Control", "public, max-age=60".parse().unwrap());
return Ok(Json(product));
}
// Fetch from database (simulated)
let product = Product {
id,
name: format!("Product {}", id),
price: 99.99,
};
// Cache the result
cache.insert(id, product.clone()).await;
res.headers_mut().insert("X-Cache", "MISS".parse().unwrap());
res.headers_mut().insert("Cache-Control", "public, max-age=60".parse().unwrap());
Ok(Json(product))
}
#[tokio::main]
async fn main() {
let cache: ProductCache = Cache::builder()
.max_capacity(10_000)
.time_to_live(Duration::from_secs(300))
.build();
let router = Router::new()
.hoop(affix_state::inject(cache))
.push(Router::with_path("products/{id}").get(get_product));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Best Practices
- Use appropriate TTL: Match cache duration to data freshness requirements
- Set cache headers: Enable browser and CDN caching
- Implement cache invalidation: Clear cache when data changes
- Use ETag for validation: Enable conditional requests
- Monitor cache hit rate: Track effectiveness
- Size cache appropriately: Balance memory usage and hit rate
- Cache at multiple layers: Browser, CDN, application, database
- Consider stale-while-revalidate: Serve stale content while refreshing