pub const DEFAULT_CACHE_CAPACITY: usize = 10_000;
pub const DEFAULT_TTL_SECS: u64 = 300;
pub const DEFAULT_CLEANUP_INTERVAL_SECS: u64 = 60;
pub const DEFAULT_EVICTION_THRESHOLD: f64 = 0.9;
use ahash::AHashMap as HashMap;
use dashmap::DashMap;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tracing::{debug, trace, warn};
use crate::error::StorageError;
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub value: String,
pub expires_at: Option<Instant>,
pub last_accessed: Instant,
pub access_count: u64,
}
impl CacheEntry {
pub fn new(value: String, ttl: Option<Duration>) -> Self {
let expires_at = ttl.map(|d| Instant::now() + d);
Self {
value,
expires_at,
last_accessed: Instant::now(),
access_count: 1,
}
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Instant::now() > expires_at
} else {
false
}
}
pub fn update_access(&mut self) {
self.last_accessed = Instant::now();
self.access_count += 1;
}
}
struct SingleFlightLoader {
pending: DashMap<String, watch::Sender<Option<Result<String, StorageError>>>>,
}
impl SingleFlightLoader {
fn new() -> Self {
Self {
pending: DashMap::new(),
}
}
async fn get_or_load<F, Fut>(&self, key: &str, loader: F) -> Result<String, StorageError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<String, StorageError>>,
{
use dashmap::mapref::entry::Entry;
let key_owned = key.to_string();
match self.pending.entry(key_owned.clone()) {
Entry::Occupied(entry) => {
trace!("等待其他请求加载 key={}", key);
let tx = entry.get();
let mut rx = tx.subscribe();
drop(entry);
if let Some(res) = rx.borrow().clone() {
return res;
}
let timeout = tokio::time::timeout(Duration::from_secs(30), rx.changed());
match timeout.await {
Ok(Ok(())) => {
if let Some(res) = rx.borrow().clone() {
return res;
}
}
Ok(Err(_)) | Err(_) => {
warn!("加载超时或 channel 关闭,清理 pending 条目: key={}", key);
self.pending.remove(&key_owned);
return Err(StorageError::TimeoutError(
"Loader timeout or channel closed".to_string(),
));
}
}
self.pending.remove(&key_owned);
Err(StorageError::TimeoutError(
"Loader dropped without result".to_string(),
))
}
Entry::Vacant(entry) => {
let (tx, _) = watch::channel(None);
entry.insert(tx.clone());
let result = loader().await;
let _ = tx.send(Some(result.clone()));
self.pending.remove(&key_owned);
result
}
}
}
}
#[derive(Debug, Clone)]
pub struct L2CacheConfig {
pub capacity: usize,
pub default_ttl: Option<Duration>,
pub cleanup_interval: Duration,
pub eviction_threshold: f64,
}
impl Default for L2CacheConfig {
fn default() -> Self {
Self {
capacity: DEFAULT_CACHE_CAPACITY,
default_ttl: Some(Duration::from_secs(DEFAULT_TTL_SECS)),
cleanup_interval: Duration::from_secs(DEFAULT_CLEANUP_INTERVAL_SECS),
eviction_threshold: DEFAULT_EVICTION_THRESHOLD,
}
}
}
impl L2CacheConfig {
pub fn new() -> Self {
Self::default()
}
pub fn capacity(mut self, capacity: usize) -> Self {
if capacity == 0 {
warn!("缓存容量设置为0,将使用最小值1");
self.capacity = 1;
} else if capacity > 1_000_000 {
warn!("缓存容量过大({}),可能影响性能", capacity);
self.capacity = capacity;
} else {
self.capacity = capacity;
}
self
}
pub fn default_ttl(mut self, ttl: Duration) -> Self {
if ttl.as_secs() == 0 {
warn!("默认TTL不能为0,将使用默认值");
self.default_ttl = Some(Duration::from_secs(DEFAULT_TTL_SECS));
} else if ttl.as_secs() > 86400 * 30 {
warn!("默认TTL过大({:?}),超过30天", ttl);
self.default_ttl = Some(ttl);
} else {
self.default_ttl = Some(ttl);
}
self
}
pub fn cleanup_interval(mut self, interval: Duration) -> Self {
if interval.as_secs() < 10 {
warn!("清理间隔过短({:?}),最小值为10秒", interval);
self.cleanup_interval = Duration::from_secs(10);
} else if interval.as_secs() > 3600 {
warn!("清理间隔过长({:?}),最大值为1小时", interval);
self.cleanup_interval = Duration::from_secs(3600);
} else {
self.cleanup_interval = interval;
}
self
}
pub fn eviction_threshold(mut self, threshold: f64) -> Self {
if !(0.0..=1.0).contains(&threshold) {
warn!("淘汰阈值{}超出范围[0.0, 1.0],将使用默认值", threshold);
self.eviction_threshold = DEFAULT_EVICTION_THRESHOLD;
} else {
self.eviction_threshold = threshold;
}
self
}
pub fn validate(&self) -> Result<(), String> {
if self.capacity == 0 {
return Err("缓存容量不能为0".to_string());
}
if !(0.0..=1.0).contains(&self.eviction_threshold) {
return Err("淘汰阈值必须在[0.0, 1.0]范围内".to_string());
}
if let Some(ttl) = self.default_ttl {
if ttl.as_secs() == 0 {
return Err("默认TTL不能为0".to_string());
}
}
Ok(())
}
}
pub struct L2Cache {
data: Arc<tokio::sync::Mutex<lru::LruCache<String, CacheEntry>>>,
single_flight: Arc<SingleFlightLoader>,
config: L2CacheConfig,
__stats: Arc<CacheStats>,
cleanup_handle: Option<JoinHandle<()>>,
}
#[derive(Debug, Default, Clone)]
pub struct CacheStats {
inner: Arc<std::sync::Mutex<StatsData>>,
}
#[derive(Debug, Default, Clone)]
struct StatsData {
hits: u64,
misses: u64,
expirations: u64,
evictions: u64,
writes: u64,
}
impl CacheStats {
pub fn new() -> Self {
Self {
inner: Arc::new(std::sync::Mutex::new(StatsData::default())),
}
}
pub fn record_hit(&self) {
let mut stats = self.inner.lock().unwrap();
stats.hits += 1;
}
pub fn record_miss(&self) {
let mut stats = self.inner.lock().unwrap();
stats.misses += 1;
}
pub fn record_expiration(&self) {
let mut stats = self.inner.lock().unwrap();
stats.expirations += 1;
}
pub fn record_eviction(&self) {
let mut stats = self.inner.lock().unwrap();
stats.evictions += 1;
}
pub fn record_write(&self) {
let mut stats = self.inner.lock().unwrap();
stats.writes += 1;
}
pub fn hits(&self) -> u64 {
self.inner.lock().unwrap().hits
}
pub fn misses(&self) -> u64 {
self.inner.lock().unwrap().misses
}
pub fn expirations(&self) -> u64 {
self.inner.lock().unwrap().expirations
}
pub fn evictions(&self) -> u64 {
self.inner.lock().unwrap().evictions
}
pub fn writes(&self) -> u64 {
self.inner.lock().unwrap().writes
}
pub fn hit_rate(&self) -> f64 {
let stats = self.inner.lock().unwrap();
let total = stats.hits + stats.misses;
if total == 0 {
0.0
} else {
stats.hits as f64 / total as f64
}
}
}
impl L2Cache {
pub fn new(capacity: usize, cleanup_interval: Duration) -> Self {
Self::with_config(L2CacheConfig {
capacity,
cleanup_interval,
..Default::default()
})
}
pub fn with_config(config: L2CacheConfig) -> Self {
if let Err(e) = config.validate() {
panic!("L2Cache配置无效: {}", e);
}
let stats = Arc::new(CacheStats::new());
let single_flight = Arc::new(SingleFlightLoader::new());
let cleanup_handle = Self::start_cleanup_task(Arc::clone(&stats), config.cleanup_interval);
Self {
data: Arc::new(tokio::sync::Mutex::new(lru::LruCache::new(
NonZeroUsize::new(config.capacity).unwrap_or(NonZeroUsize::new(1).unwrap()),
))),
single_flight,
config,
__stats: stats,
cleanup_handle: Some(cleanup_handle),
}
}
fn start_cleanup_task(__stats: Arc<CacheStats>, interval: Duration) -> JoinHandle<()> {
tokio::spawn(async move {
let mut cleanup_interval = tokio::time::interval(interval);
loop {
cleanup_interval.tick().await;
debug!("执行缓存清理任务");
}
})
}
pub async fn get(&self, key: &str) -> Option<String> {
let mut cache = self.data.lock().await;
if let Some(entry) = cache.get_mut(key) {
if entry.is_expired() {
cache.pop(key);
self.__stats.record_expiration();
return None;
}
entry.update_access();
self.__stats.record_hit();
Some(entry.value.clone())
} else {
self.__stats.record_miss();
None
}
}
pub async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) {
let ttl = ttl.or(self.config.default_ttl);
let entry = CacheEntry::new(value.to_string(), ttl);
let mut cache = self.data.lock().await;
if cache.len() >= self.config.capacity {
if cache.len() >= self.config.capacity {
self.__stats.record_eviction();
}
}
cache.put(key.to_string(), entry);
self.__stats.record_write();
}
pub async fn delete(&self, key: &str) {
let mut cache = self.data.lock().await;
cache.pop(key);
}
pub async fn contains(&self, key: &str) -> bool {
let mut cache = self.data.lock().await;
if let Some(entry) = cache.get(key) {
!entry.is_expired()
} else {
false
}
}
pub async fn clear(&self) {
let mut cache = self.data.lock().await;
cache.clear();
}
pub async fn len(&self) -> usize {
let cache = self.data.lock().await;
cache.len()
}
pub async fn is_empty(&self) -> bool {
let cache = self.data.lock().await;
cache.is_empty()
}
pub async fn get_or_load<F, Fut>(&self, key: &str, loader: F) -> Result<String, StorageError>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<String, StorageError>>,
{
if let Some(value) = self.get(key).await {
return Ok(value);
}
let value = self.single_flight.get_or_load(key, loader).await?;
self.set(key, &value, self.config.default_ttl).await;
Ok(value)
}
pub async fn batch_get(&self, keys: &[String]) -> HashMap<String, String> {
let mut result = HashMap::new();
for key in keys {
if let Some(value) = self.get(key).await {
result.insert(key.clone(), value);
}
}
result
}
pub async fn batch_set(&self, items: &[(String, String, Option<Duration>)]) {
for (key, value, ttl) in items {
self.set(key, value, *ttl).await;
}
}
pub async fn batch_delete(&self, keys: &[String]) {
for key in keys {
self.delete(key).await;
}
}
pub async fn cleanup_expired(&self) -> usize {
let mut cache = self.data.lock().await;
let expired_keys: Vec<String> = cache
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(key, _)| key.clone())
.collect();
let count = expired_keys.len();
for key in expired_keys {
cache.pop(&key);
self.__stats.record_expiration();
}
if count > 0 {
debug!("清理了 {} 条过期数据", count);
}
count
}
#[allow(dead_code)]
async fn evict_lru(&self) {
let mut cache = self.data.lock().await;
if cache.pop_lru().is_some() {
self.__stats.record_eviction();
debug!("LRU淘汰成功");
}
}
pub fn stats(&self) -> &CacheStats {
&self.__stats
}
pub fn config(&self) -> &L2CacheConfig {
&self.config
}
pub async fn shutdown(&self) {
if let Some(handle) = &self.cleanup_handle {
handle.abort();
}
}
}
impl Drop for L2Cache {
fn drop(&mut self) {
if let Some(handle) = self.cleanup_handle.take() {
handle.abort();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_cache_set_get() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
let value = cache.get("key1").await;
assert_eq!(value, Some("value1".to_string()));
}
#[tokio::test]
async fn test_cache_get_not_found() {
let cache = L2Cache::new(100, Duration::from_secs(60));
let value = cache.get("nonexistent").await;
assert_eq!(value, None);
}
#[tokio::test]
async fn test_cache_delete() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
cache.delete("key1").await;
let value = cache.get("key1").await;
assert_eq!(value, None);
}
#[tokio::test]
async fn test_cache_ttl() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache
.set("key1", "value1", Some(Duration::from_millis(100)))
.await;
tokio::time::sleep(Duration::from_millis(150)).await;
let value = cache.get("key1").await;
assert_eq!(value, None);
}
#[tokio::test]
async fn test_cache_contains() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
assert!(cache.contains("key1").await);
assert!(!cache.contains("nonexistent").await);
}
#[tokio::test]
async fn test_cache_clear() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
cache.set("key2", "value2", None).await;
cache.clear().await;
assert!(cache.is_empty().await);
assert_eq!(cache.len().await, 0);
}
#[tokio::test]
async fn test_batch_operations() {
let cache = L2Cache::new(100, Duration::from_secs(60));
let items = vec![
("key1".to_string(), "value1".to_string(), None),
("key2".to_string(), "value2".to_string(), None),
("key3".to_string(), "value3".to_string(), None),
];
cache.batch_set(&items).await;
let keys = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
let result = cache.batch_get(&keys).await;
assert_eq!(result.len(), 3);
assert_eq!(result.get("key1"), Some(&"value1".to_string()));
let delete_keys = vec!["key1".to_string(), "key2".to_string()];
cache.batch_delete(&delete_keys).await;
assert_eq!(cache.len().await, 1);
}
#[tokio::test]
async fn test_single_flight() {
let cache = L2Cache::new(100, Duration::from_secs(60));
let load_count = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let load_count_clone = load_count.clone();
let loader = || {
let load_count = load_count_clone.clone();
async move {
load_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(100)).await;
Ok("loaded_value".to_string())
}
};
let task1 = cache.get_or_load("key1", loader);
let task2 = cache.get_or_load("key1", loader);
let task3 = cache.get_or_load("key1", loader);
let (r1, r2, r3) = tokio::join!(task1, task2, task3);
assert!(r1.is_ok());
assert!(r2.is_ok());
assert!(r3.is_ok());
assert_eq!(load_count.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_cache_stats() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
cache.get("key1").await; cache.get("key2").await;
let stats = cache.stats();
assert_eq!(stats.hits(), 1);
assert_eq!(stats.misses(), 1);
assert_eq!(stats.hit_rate(), 0.5);
}
#[tokio::test]
async fn test_cleanup_expired() {
let cache = L2Cache::new(100, Duration::from_secs(60));
cache
.set("key1", "value1", Some(Duration::from_millis(100)))
.await;
cache.set("key2", "value2", None).await;
tokio::time::sleep(Duration::from_millis(150)).await;
let cleaned = cache.cleanup_expired().await;
assert_eq!(cleaned, 1);
assert_eq!(cache.len().await, 1);
}
#[tokio::test]
async fn test_lru_eviction() {
let cache = L2Cache::new(3, Duration::from_secs(60));
cache.set("key1", "value1", None).await;
cache.set("key2", "value2", None).await;
cache.set("key3", "value3", None).await;
cache.get("key1").await;
cache.get("key2").await;
cache.set("key4", "value4", None).await;
assert_eq!(cache.len().await, 3);
assert!(cache.contains("key1").await);
assert!(cache.contains("key2").await);
assert!(!cache.contains("key3").await);
assert!(cache.contains("key4").await);
}
#[tokio::test]
async fn test_config_builder() {
let config = L2CacheConfig::new()
.capacity(5000)
.default_ttl(Duration::from_secs(600))
.cleanup_interval(Duration::from_secs(30))
.eviction_threshold(0.8);
assert_eq!(config.capacity, 5000);
assert_eq!(config.default_ttl, Some(Duration::from_secs(600)));
assert_eq!(config.cleanup_interval, Duration::from_secs(30));
assert_eq!(config.eviction_threshold, 0.8);
}
#[tokio::test]
async fn test_concurrent_access() {
let cache = Arc::new(L2Cache::new(1000, Duration::from_secs(60)));
let mut handles = vec![];
for i in 0..100 {
let cache_clone = Arc::clone(&cache);
let handle = tokio::spawn(async move {
let key = format!("key{}", i);
let value = format!("value{}", i);
cache_clone.set(&key, &value, None).await;
cache_clone.get(&key).await
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
assert_eq!(cache.len().await, 100);
}
}