Skip to main content

tibba_cache/
ttl_lru_store.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use lru::LruCache;
16use std::num::NonZeroUsize;
17use tokio::sync::RwLock;
18
19/// 用于判断缓存数据是否已过期的 trait。
20pub trait Expired {
21    /// 返回 `true` 表示数据已过期,应从缓存中移除。
22    fn is_expired(&self) -> bool;
23}
24
25/// 线程安全的 TTL + LRU 两级淘汰缓存存储。
26/// 同时支持多读或单写并发访问。
27pub struct TtlLruStore<T> {
28    cache: RwLock<LruCache<String, T>>,
29}
30
31impl<T: Expired + Clone> TtlLruStore<T> {
32    /// 创建指定容量的 TtlLruStore,容量必须大于 0。
33    pub fn new(size: NonZeroUsize) -> Self {
34        Self {
35            cache: RwLock::new(LruCache::new(size)),
36        }
37    }
38
39    /// 向缓存写入键值对。容量已满时自动淘汰最久未使用的条目。
40    pub async fn set(&self, key: &str, value: T) {
41        let mut cache = self.cache.write().await;
42        cache.put(key.to_string(), value);
43    }
44
45    /// 读取未过期的缓存值,键不存在或已过期时返回 `None`。
46    /// 内部使用 peek 而非 get,不更新 LRU 顺序,性能更优。
47    pub async fn get(&self, key: &str) -> Option<T> {
48        let cache = self.cache.read().await;
49        // 使用 peek 避免更新 LRU 顺序,读多写少场景性能更佳
50        cache.peek(key).filter(|v| !v.is_expired()).cloned()
51    }
52
53    /// 删除指定键,键不存在时为空操作。
54    pub async fn del(&self, key: &str) {
55        let mut cache = self.cache.write().await;
56        cache.pop(key);
57    }
58
59    /// 清除所有已过期的条目,应定期调用以释放内存。
60    pub async fn purge_expired(&self) {
61        let mut cache = self.cache.write().await;
62        // LruCache 不支持迭代中删除,需先收集过期键再批量移除
63        let keys: Vec<String> = cache
64            .iter()
65            .filter(|(_, v)| v.is_expired())
66            .map(|(k, _)| k.clone())
67            .collect();
68        for key in keys {
69            cache.pop(&key);
70        }
71    }
72}