题目
实现一个异步缓存结构
信息
- 类型:问答
- 难度:⭐⭐
考点
异步编程,共享状态,错误处理,性能优化
快速回答
实现一个线程安全的异步缓存结构需要关注:
- 使用
Arc<Mutex<T>>或Arc<RwLock<T>>实现内部状态共享 - 通过
async函数封装获取逻辑 - 处理可能的竞态条件(如缓存击穿)
- 使用
Option或Result处理缺失值 - 考虑添加 TTL 过期机制
问题场景
在异步系统中,频繁访问外部资源(如数据库或API)会导致性能瓶颈。我们需要实现一个缓存结构:
- 当多个任务同时请求相同 key 时,只执行一次实际加载操作
- 自动处理并发访问的同步问题
- 支持异步获取值
核心实现方案
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub struct AsyncCache<K, V> {
inner: Arc<RwLock<HashMap<K, CacheItem<V>>>>,
}
struct CacheItem<V> {
value: V,
expires_at: Option<Instant>,
}
impl<K, V> AsyncCache<K, V>
where
K: Eq + std::hash::Hash + Clone + Send + 'static,
V: Clone + Send + 'static,
{
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
}
}
// 核心异步获取方法
pub async fn get_or_init<F, Fut>(&self, key: K, loader: F) -> Result<V, String>
where
F: FnOnce(K) -> Fut,
Fut: std::future::Future<Output = Result<V, String>>,
{
// 首先尝试读取缓存
{
let cache = self.inner.read().await;
if let Some(item) = cache.get(&key) {
if item.is_valid() {
return Ok(item.value.clone());
}
}
}
// 获取写锁准备加载数据
let mut cache = self.inner.write().await;
// 双重检查避免在等待锁时值已被加载
if let Some(item) = cache.get(&key) {
if item.is_valid() {
return Ok(item.value.clone());
}
}
// 执行异步加载函数
let value = loader(key.clone()).await?;
// 插入新值(实际项目可添加TTL逻辑)
cache.insert(
key,
CacheItem {
value: value.clone(),
expires_at: Some(Instant::now() + Duration::from_secs(30)),
},
);
Ok(value)
}
}
impl<V> CacheItem<V> {
fn is_valid(&self) -> bool {
match self.expires_at {
Some(expiry) => expiry > Instant::now(),
None => true,
}
}
}关键设计点解析
- 锁的选择:使用
RwLock而非Mutex提高读并发性能 - 双重检查锁定:在获取写锁后再次检查缓存,避免重复加载
- TTL 机制:通过
Instant记录过期时间,is_valid()方法验证有效性 - 闭包设计:
loader参数接收异步闭包,保证加载逻辑可定制
最佳实践
- 错误传递:使用
Result传播加载过程中的错误 - 内存控制:定期清理过期项目(可添加后台任务)
- 性能优化:对高频 key 使用
entry API优化查找效率 - 死锁预防:避免在持有锁时执行 await 操作(本例中 loader 在释放锁后执行)
常见错误
- 缓存击穿:多个请求同时未命中导致重复加载(双重检查解决)
- 锁粒度问题:在 loader 执行期间持有写锁会阻塞所有请求(正确做法是先释放锁再执行 loader)
- 生命周期错误:未正确处理
'static约束导致编译失败 - 阻塞异步运行时:在异步上下文中使用同步锁导致线程阻塞
扩展知识
- 缓存淘汰策略:LRU/LFU 实现(可考虑使用
linked-hash-mapcrate) - 分布式缓存:结合 Redis 等外部存储实现多节点共享
- 性能监控:添加命中率/加载时间等 metrics
- 高级模式:使用
futures::future::Shared实现请求合并