Skip to main content

tibba_cache/
two_level_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 super::{Error, Expired, RedisCache, TtlLruStore};
16use serde::{Serialize, de::DeserializeOwned};
17use std::num::NonZeroUsize;
18use std::time::{Duration, SystemTime, UNIX_EPOCH};
19
20type Result<T> = std::result::Result<T, Error>;
21
22#[inline]
23fn now_secs() -> u64 {
24    SystemTime::now()
25        .duration_since(UNIX_EPOCH)
26        .unwrap_or_default()
27        .as_secs()
28}
29
30/// 计算与间隔边界对齐的 TTL,防止大量缓存在同一时刻集中失效(缓存雪崩)。
31/// 若剩余时间不足 unit 的 1/10,则延伸至下一个间隔周期。
32#[inline]
33fn get_ttl_by_unit(unit: Duration) -> Duration {
34    let secs = unit.as_secs();
35    if secs == 0 {
36        return Duration::ZERO;
37    }
38    // 计算距下一个间隔边界的剩余秒数
39    let remaining = secs - (now_secs() % secs);
40    // 剩余时间不足 1/10 时延伸到下一周期,避免刚写入就立刻过期
41    if remaining < secs / 10 {
42        return Duration::from_secs(secs + remaining);
43    }
44    Duration::from_secs(remaining)
45}
46
47/// 为缓存数据附加过期时间戳的包装结构体。
48#[derive(Clone)]
49struct ExpiredCache<T> {
50    /// 实际缓存的数据
51    data: T,
52    /// 缓存条目的过期 Unix 时间戳(秒)
53    expired_at: u64,
54}
55
56impl<T> Expired for ExpiredCache<T> {
57    /// 当前时间已达到或超过 expired_at 时返回 `true`。
58    fn is_expired(&self) -> bool {
59        now_secs() >= self.expired_at
60    }
61}
62
63/// 内存 LRU + Redis 双层缓存。
64/// 读操作优先命中内存 LRU,未命中再查 Redis 并回填内存层。
65pub struct TwoLevelStore<T> {
66    /// 第一层:带 TTL 的内存 LRU 缓存
67    lru: TtlLruStore<ExpiredCache<T>>,
68    /// 缓存条目的默认 TTL
69    ttl: Duration,
70    /// 第二层:Redis 缓存
71    redis: RedisCache,
72}
73
74impl<T: Clone + Serialize + DeserializeOwned> TwoLevelStore<T> {
75    /// 创建新的 TwoLevelStore 实例。
76    /// `size` 为内存 LRU 的最大条目数,`ttl` 为缓存默认过期时长。
77    pub fn new(redis: RedisCache, size: NonZeroUsize, ttl: Duration) -> Self {
78        Self {
79            lru: TtlLruStore::new(size),
80            ttl,
81            redis,
82        }
83    }
84
85    async fn fill_lru(&self, key: &str, value: T, ttl: Duration) {
86        if ttl.is_zero() {
87            return;
88        }
89        self.lru
90            .set(
91                key,
92                ExpiredCache {
93                    data: value,
94                    expired_at: now_secs() + ttl.as_secs(),
95                },
96            )
97            .await;
98    }
99
100    /// 将值写入两层缓存。
101    /// TTL 与间隔边界对齐,先写 Redis 再更新内存 LRU。
102    pub async fn set(&self, key: &str, value: T) -> Result<()> {
103        // 计算对齐后的 TTL
104        let ttl = get_ttl_by_unit(self.ttl);
105
106        // 先写入 Redis,再更新内存缓存
107        self.redis.set_struct(key, &value, Some(ttl)).await?;
108        self.fill_lru(key, value, ttl).await;
109
110        Ok(())
111    }
112
113    /// 从缓存读取值,优先查询内存 LRU,未命中则查 Redis 并回填 LRU。
114    /// 若 Redis 中条目剩余 TTL 已超出预期范围则不回填内存层。
115    pub async fn get(&self, key: &str) -> Result<Option<T>> {
116        // 优先查内存 LRU(已过期条目不会被返回)
117        if let Some(value) = self.lru.get(key).await {
118            return Ok(Some(value.data));
119        }
120        // 内存未命中,查 Redis
121        let result: Option<T> = self.redis.get_struct(key).await?;
122        if let Some(value) = &result {
123            let ttl = get_ttl_by_unit(self.ttl);
124            // TTL 超出预期范围说明条目即将过期,不回填内存缓存
125            if ttl <= self.ttl {
126                self.fill_lru(key, value.clone(), ttl).await;
127            }
128        }
129        Ok(result)
130    }
131
132    /// 从两层缓存中删除指定键。
133    pub async fn del(&self, key: &str) -> Result<()> {
134        self.lru.del(key).await;
135        self.redis.del(key).await
136    }
137
138    /// 清除内存 LRU 中的过期条目,应定期调用以释放内存。
139    /// Redis 侧的 TTL 由 Redis 自身管理,无需手动清理。
140    pub async fn purge_expired(&self) {
141        self.lru.purge_expired().await;
142    }
143}