TTL是Time To Live的缩写,通常意味着元素的生存时间是多长。
应用场景
- 数据库:在redis中我们最常见的就是缓存我们的数据元素,但是我们又不想其保留太长的时间,因为数据时间越长污染的可能性就越大,我们又不想在后续的程序中设置删除,所以我们此时需要设置过期时间来让数据自动淘汰。
setex now 10000 algorithm-rs
- 内存缓存:通常在程序中需要缓存一定的数据结果,但是因为内存是有限的,需要在内存中储存最有效的数据进行缓存,此时需要设置过期时间,以在规定时间内淘汰无用的数据。
带ttl的Lru算法的优缺点
- 优点:
- 可以根据过期时间自动淘汰掉无用的数据。
- 缺点:
- 需要维护过期时间字段
- 需要额外的cpu进行数据对比及可能出现的大量数据淘汰要额外️的进行cpu运算去淘汰数据。
了解Rust中的feature
在Rust编程语言中,feature
是一个在Cargo.toml
文件中定义的配置项,它允许开发者在构建和依赖项选择方面进行更细粒度的控制。
feature
类似于C/C++
中的#ifdef
,我们可以根据需求来启用或者关闭代码,这样子可以有效的达到我们想要的功能。
在此设计中,我们在Cargo.toml
定义了ttl
的feature
来启用ttl的功能。
在代码中我们可以在函数,也可以在某字段,也可以在某个执行中定义#[cfg(feature = "ttl")]
,他生效的是下一个字段或者函数或者语句
结构变化
在每个结点中,添加ttl的feature
pub(crate) struct LruEntry { /// 头部节点及尾部结点均未初始化值 pub key: mem::MaybeUninit, /// 头部节点及尾部结点均未初始化值 pub val: mem::MaybeUninit, pub prev: *mut LruEntry, pub next: *mut LruEntry, /// 带ttl的过期时间,单位秒 /// 如果为u64::MAX,则表示不过期 #[cfg(feature = "ttl")] pub expire: u64, }
在此处我们每个结点添加了一个u64的过期时间。
pub struct LruCache { // ... #[cfg(feature = "ttl")] check_next: u64, /// 每次大检查点的时间间隔,如果不想启用该特性,可以将该值设成u64::MAX #[cfg(feature = "ttl")] check_step: u64, /// 所有节点中是否存在带ttl的结点,如果均为普通的元素,则过期的将不进行检查 #[cfg(feature = "ttl")] has_ttl: bool, }
函数变化
我们在获取元素结点时,需要判断其是否过期再进行返回,如果过期我们将返回空并将该结点进行删除。
pub(crate) fn get_node(&mut self, k: &Q) -> Option<*mut LruEntry>
where K: Borrow,
Q: Hash + Eq + ?Sized, { match self.map.get(KeyWrapper::from_ref(k)) { Some(l) => { let node = l.as_ptr(); self.detach(node); #[cfg(feature = "ttl")] unsafe { if self.has_ttl && (*node).is_expire() { self.map.remove(KeyWrapper::from_ref(k)); let _ = *Box::from_raw(node); return None; } } self.attach(node); Some(node) } None => None, } }
其中is_expire
将会获取系统时间来进行当前是否过期的对比
#[cfg(feature = "ttl")] #[inline(always)] pub fn is_expire(&self) -> bool { get_timestamp() >= self.expire } #[inline(always)] pub fn get_timestamp() -> u64 { SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).expect("ok").as_secs() }
我们将这种函数代码量极少的进行内联的声明,以牺牲二进制包大小来提高运行速度。
插入方法我们额外提供带ttl的数据插入:
/// 插入带有生存时间的元素 /// 每次获取像redis一样,并不会更新生存时间 /// 如果需要更新则需要手动的进行重新设置 #[inline(always)] pub fn insert_with_ttl(&mut self, k: K, v: V, ttl: u64) -> Option { self.has_ttl = true; self._capture_insert_with_ttl(k, v, ttl).map(|(_, v, _)| v) } #[allow(unused_variables)] fn _capture_insert_with_ttl(&mut self, k: K, mut v: V, ttl: u64) -> Option<(K, V, bool)> { #[cfg(feature="ttl")] self.clear_expire(); let key = KeyRef::new(&k); match self.map.get_mut(&key) { Some(entry) => { let entry_ptr = entry.as_ptr(); unsafe { mem::swap(&mut *(*entry_ptr).val.as_mut_ptr(), &mut v); } #[cfg(feature="ttl")] unsafe { (*entry_ptr).expire = ttl.saturating_add(get_timestamp()); } self.detach(entry_ptr); self.attach(entry_ptr); Some((k, v, true)) } None => { let (val, entry) = self.replace_or_create_node(k, v); let entry_ptr = entry.as_ptr(); self.attach(entry_ptr); #[cfg(feature="ttl")] unsafe { (*entry_ptr).expire = ttl.saturating_add(get_timestamp()); } unsafe { self.map .insert(KeyRef::new((*entry_ptr).key.as_ptr()), entry); } val.map(|(k, v)| (k, v, false)) } } }
我们在插入的同时,会将过期时间进行设置,不带ttl的我们同样走该方法,只是传入的ttl参数ttl: u64
将不会被使用,我们这里声明了#[allow(unused_variables)]
告诉编译器,我们这里变量没有使用是我们预料之中的,不要进行警告。
我们将会设置节点的过期时间:
#[cfg(feature="ttl")] unsafe { (*entry_ptr).expire = ttl.saturating_add(get_timestamp()); }
清除策略
Redis中过期数据的清除策略主要有三种:惰性删除、定时删除和定期删除。这些策略在Redis中用于平衡内存占用与CPU使用之间的关系,以确保Redis的性能和稳定性。
在这里我们实现的是惰性删除及定期删除策略,但是每次定期删除可能会遍历所有的元素,如果数据太大,容易无法在规定的时间内进行数据清理。后续可能需要单次最大遍历数据数量。
惰性删除
我们将获取元素的时候统一进行检查get_node
,所有相关获取的数据将全部调用这里,这样子将函数统一化,可以更好的优化代码。
定期删除
每次执行会获取一次系统函数时间。
#[cfg(feature="ttl")] pub fn clear_expire(&mut self) { if !self.has_ttl { return; } let now = get_timestamp(); if now < self.check_next { return; } self.check_next = now + self.check_step; unsafe { let mut ptr = self.tail; while ptr != self.head { if (*ptr).is_little(&now) { let next = (*ptr).prev; self.detach(ptr); self.map.remove(&KeyRef::new(&*(*ptr).key.as_ptr())); let _ = *Box::from_raw(ptr); ptr = next; } else { ptr = (*ptr).prev; } } } }
在清除的时候,需要先将map的数据移除掉,因为map的key只是节点的一个引用,如果先将节点删除,那么将出现map中的key指针悬空的情况。
self.map.remove(&KeyRef::new(&*(*ptr).key.as_ptr())); let _ = *Box::from_raw(ptr);
在上述代码中,两行函数不能被调换,否则将无法正确删除map中的数据。
其它操作
set_ttl
设置元素的生存时间del_ttl
删除元素的生存时间,表示永不过期get_ttl
获取元素的生存时间set_check_step
设置当前检查lru的间隔- 其它Lru能进行操作的均能操作
示例
以下示例示范当数据过期时,在获取元素将为空,演示了惰性删除。
#[test] #[cfg(feature="ttl")] fn test_ttl_cache() { let mut lru = LruCache::new(3); lru.insert_with_ttl("help", "ok", 1); lru.insert_with_ttl("author", "tickbh", 2); assert_eq!(lru.len(), 2); std::thread::sleep(std::time::Duration::from_secs(1)); assert_eq!(lru.get("help"), None); std::thread::sleep(std::time::Duration::from_secs(1)); assert_eq!(lru.get("author"), None); assert_eq!(lru.len(), 0); }
以下演示以定时删除,将在插入及定时到的时候进行删除数据。
#[test] #[cfg(feature="ttl")] fn test_ttl_check_cache() { let mut lru = LruCache::new(3); lru.set_check_step(1); lru.insert_with_ttl("help", "ok", 1); lru.insert("now", "algorithm"); assert_eq!(lru.len(), 2); std::thread::sleep(std::time::Duration::from_secs(1)); assert_eq!(lru.len(), 2); lru.insert_with_ttl("author", "tickbh", 3); assert_eq!(lru.len(), 2); assert_eq!(lru.get("help"), None); assert_eq!(lru.len(), 2); }
完整项目地址
https://github.com/tickbh/algorithm-rs
结语
带ttl的Lru可以一定程序上补充缓存的可用性。更方便的让您操作缓存。将内存与命中率进行完美的结合。