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定义了ttlfeature来启用ttl的功能。

在代码中我们可以在函数,也可以在某字段,也可以在某个执行中定义#[cfg(feature = "ttl")],他生效的是下一个字段或者函数或者语句

结构变化

在每个结点中,添加ttl的feature

pub(crate) struct LruEntry<K, V> {
/// 头部节点及尾部结点均未初始化值
pub key: mem::MaybeUninit<K>,
/// 头部节点及尾部结点均未初始化值
pub val: mem::MaybeUninit<V>,
pub prev: *mut LruEntry<K, V>,
pub next: *mut LruEntry<K, V>,
/// 带ttl的过期时间,单位秒
/// 如果为u64::MAX,则表示不过期
#[cfg(feature = "ttl")]
pub expire: u64,
}

在此处我们每个结点添加了一个u64的过期时间。

pub struct LruCache<K, V, S> {
// ...
#[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<Q>(&mut self, k: &Q) -> Option<*mut LruEntry<K, V>>
where
K: Borrow<Q>,
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<V> {
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可以一定程序上补充缓存的可用性。更方便的让您操作缓存。将内存与命中率进行完美的结合。

带有ttl的Lru在Rust中的实现及源码解析的更多相关文章

  1. Django框架rest_framework中APIView的as_view()源码解析、认证、权限、频率控制

    在上篇我们对Django原生View源码进行了局部解析:https://www.cnblogs.com/dongxixi/p/11130976.html 在前后端分离项目中前面我们也提到了各种认证需要 ...

  2. 6.2 dubbo在spring中自定义xml标签源码解析

    在6.1 如何在spring中自定义xml标签中我们看到了在spring中自定义xml标签的方式.dubbo也是这样来实现的. 一 META_INF/dubbo.xsd 比较长,只列出<dubb ...

  3. Scala 深入浅出实战经典 第60讲:Scala中隐式参数实战详解以及在Spark中的应用源码解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

  4. RocketMQ中Broker的消息存储源码分析

    Broker和前面分析过的NameServer类似,需要在Pipeline责任链上通过NettyServerHandler来处理消息 [RocketMQ中NameServer的启动源码分析] 实际上就 ...

  5. Springboot中mybatis执行逻辑源码分析

    Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...

  6. 在Xcode中使用Git进行源码版本控制

    http://www.cocoachina.com/ios/20140524/8536.html 资讯 论坛 代码 工具 招聘 CVP 外快 博客new 登录| 注册   iOS开发 Swift Ap ...

  7. Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

    欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...

  8. Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

  9. Scala 深入浅出实战经典 第61讲:Scala中隐式参数与隐式转换的联合使用实战详解及其在Spark中的应用源码解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载: 百度云盘:http://pan.baidu.com/s/1c0noOt ...

  10. Scala 深入浅出实战经典 第48讲:Scala类型约束代码实战及其在Spark中的应用源码解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

随机推荐

  1. WPF 获取本机所有字体拿到每个字符的宽度和高度

    本文主要采用 GlyphTypeface 类尝试获取每个字符的宽度和高度的值,尽管这个方法和最终 WPF 布局使用的文本的宽度和高度是不相同的,但是依然可以作为参考 获取系统字体文件夹的文件 系统字体 ...

  2. 手把手教你如何构建 WPF 官方开源框架源代码

    从去年微软就将 WPF 开源了,差不多现在所有 WPF 的源代码都开源了.在学习框架的时候,我会做一些改动,期望能构建一个自己的版本进行测试.但是作为一个特别大的框架,想要构建跑起来可不是直接在 Vi ...

  3. IIS相关发布错误解决记录

    HRESULT 代码 0x80070021 错误消息: 应用程序"应用程序名称"中的服务器错误HTTP 错误 500.19 - 内部服务器错误HRESULT:0x80070021对 ...

  4. ide构建SpringMVC框架

    框架原理图如下: 1. 创建如图项目 2. 在lib中所需导入jar包 3. 配置变量 (1) (2)add library (3)选择web app libraries 4. 配置web.xml文件 ...

  5. 【GUI开发】用python爬YouTube博主信息,并开发成exe软件!

    目录 一.背景介绍 二.代码讲解 2.1 爬虫 2.2 tkinter界面 2.3 存日志 三.说明 一.背景介绍 你好,我是@马哥python说,一名10年程序猿. 最近我用python开发了一个G ...

  6. 【爬虫+情感判定+饼图+Top10高频词+词云图】"王心凌"热门弹幕python舆情分析

    目录 一.背景介绍 二.代码讲解-爬虫部分 2.1 分析弹幕接口 2.2 讲解爬虫代码 三.代码讲解-情感分析部分 3.1 整体思路 3.2 情感分析打标 3.3 统计top10高频词 3.4 绘制词 ...

  7. axios的基本

    目录 axios基本使用.html axios+vue.html axios基本使用.html <!DOCTYPE html> <html lang="en"&g ...

  8. MQTT的使用一

    MQTT:物联网消息传递标准 简介 MQTT是用于物联网(IoT)的OASIS标准消息传递协议.它被设计为一种非常轻量级的发布/订阅消息传送,非常适合以较小的代码占用量和最小的网络带宽连接远程设备.如 ...

  9. 移动端termux安装kali

    1.相关准备一部安卓手机,termux,NVAC,浏览器2.安装kali首先进入kali的官网选择文档找到Android手机上的kali找到NetHunter-Rootless找到kali安装命令:t ...

  10. C 语言编程 — 基本语法

    目录 文章目录 目录 前文列表 C 语言 C 语言的版本 C 语言的特点 C 语言的优点 C 语言的缺点 搭建编程环境 基本语法 前文列表 <程序编译流程与 GCC 编译器> C 语言 C ...