缓存淘汰策略

LRU 与 LFU 缓存策略及其实现。

应用层缓存

鉴于磁盘和内存读写的差异性,DB 中低频写、高频读的数据适合放入内存中,直接供应用层读写。在项目中读取用户资料时就使用到了 LRU,而非放到 Redis 中。

缓存的 2 个基本实现

Set(key string, value interface) // 写数据
Get(key string) interface{}      // 读数据

缓存的 2 个特征

  • 命中率:即命中数 / 请求数,比值越高即表明缓存使用率越高,缓存更有效。
  • 淘汰策略:内存空间是有限的,当缓存数据占满内存后,若要缓存新数据,则必须淘汰一部分数据。对于 的概念,不同淘汰策略有不同原则。

下边介绍两种常用的淘汰算法:LRU 与 LFU

LRU

缩写:Least Recently Used( 最近 最久 使用),时间维度

原则:若数据在最近一段时间内都未使用(读取或更新),则以后使用几率也很低,应被淘汰。

数据结构

  • 使用链表:由于缓存读写删都是高频操作,考虑使用写删都为 O(1) 的链表,而非写删都为 O(N) 的数组。
  • 使用双链表:选用删除操作为 O(1) 的双链表而非删除为 O(N) 的单链表。
  • 维护额外哈希表:链表查找必须遍历 O(N) 读取,可在缓存中维护 map[key]*Node哈希表来实现O(1) 的链表查找。

直接使用链表节点存储缓存的 K-V 数据,链表从 head 到 tail 使用频率逐步降低。新访问数据不断追加到 head 前边,旧数据不断从 tail 剔除。LRU 使用链表顺序性保证了热数据在 head,冷数据在 tail。

双链表节点存储 K-V 数据:

type Node struct {
	key        string // 淘汰 tail 时需在维护的哈希表中删除,不是冗余存储
	val        interface{}
	prev, next *Node // 双向指针
}

type List struct {
	head, tail *Node
	size       int // 缓存空间大小
}

从上图可知,双链表需实现缓存节点新增 Prepend,剔除 Remove 操作:

func (l *List) Prepend(node *Node) *Node {
	if l.head == nil {
		l.head = node
		l.tail = node
	} else {
		node.prev = nil
		node.next = l.head
		l.head.prev = node
		l.head = node
	}
	l.size++
	return node
}

func (l *List) Remove(node *Node) *Node {
	if node == nil {
		return nil
	}
	prev, next := node.prev, node.next
	if prev == nil {
		l.head = next // 删除头结点
	} else {
		prev.next = next
	}

	if next == nil {
		l.tail = prev // 删除尾结点
	} else {
		next.prev = prev
	}

	l.size--
	node.prev, node.next = nil, nil
	return node
}

// 封装数据已存在缓存的后续操作
func (l *List) MoveToHead(node *Node) *Node {
	if node == nil {
		return nil
	}
	n := l.Remove(node)
	return l.Prepend(n)
}

func (l *List) Tail() *Node {
	return l.tail
}

func (l *List) Size() int {
	return l.size
}

LRU 操作细节

Set(k, v)

  • 数据已缓存,则更新值,挪到 head 前
  • 数据未缓存
    • 缓存空间未满:直接挪到 head 前
    • 缓存空间已满:移除 tail 并将新数据挪到 head 前

Get(k)

  • 命中:节点挪到 head 前,并返回 value
  • 未命中:返回 -1

代码实现:

type LRUCache struct {
	capacity int // 缓存空间大小
	items    map[string]*Node
	list     *List
}

func NewLRUCache(capacity int) *LRUCache {
	return &LRUCache{
		capacity: capacity,
		items:    make(map[string]*Node),
		list:     new(List),
	}
}

func (c *LRUCache) Set(k string, v interface{}) {
	// 命中
	if node, ok := c.items[k]; ok {
		node.val = v                         // 命中后更新值
		c.items[k] = c.list.MoveToHead(node) //
		return
	}

	// 未命中
	node := &Node{key: k, val: v} // 完整的 node
	if c.capacity == c.list.size {
		tail := c.list.Tail()
		delete(c.items, tail.key) // k-v 数据存储与 node 中
		c.list.Remove(tail)
	}
	c.items[k] = c.list.Prepend(node) // 更新地址
}

func (c *LRUCache) Get(k string) interface{} {
	node, ok := c.items[k]
	if ok {
		c.items[k] = c.list.MoveToHead(node)
		return node.val
	}
	return -1
}

测试

func TestLRU(t *testing.T) {
	c := NewLRUCache(2)
	c.Set(K1, 1)
	c.Set(K2, 2)
	c.Set(K1, 100)
	fmt.Println(c.Get(K1)) // 100
	c.Set(K3, 3)
	fmt.Println(c.Get(K2)) // -1
	c.Set(K4, 4)
	fmt.Println(c.Get(K1)) // -1
	fmt.Println(c.Get(K3)) // 3
	fmt.Println(c.Get(K4)) // 4
}

LFU

缩写:Least Frequently Used(最近 最少 使用),频率维度。

原则:若数据在最近一段时间内使用次数少,则以后使用几率也很低,应被淘汰。

对比 LRU,若缓存空间为 3 个数据量:

Set("2", 2)
Set("1", 1)
Get(1)
Get(2)
Set("3", 3) 
Set("4", 4) // LRU 将淘汰 1,缓存链表为 4->3->2
	    // LFU 将淘汰 3,未超出容量的时段内 1 和 2 都被使用了两次,3 仅使用一次

数据结构

依旧使用双向链表实现高效写删操作,但 LFU 淘汰原则是 使用次数,数据节点在链表中的位置与之无关。可按使用次数划分 频率梯队,数据节点使用一次就挪到高频梯队。此外维护 minFreq 表示最低梯队,维护 2 个哈希表:

  • map[freq]*List 各频率及其链表
  • map[key]*Node 实现数据节点的 O(1) 读

双链表存储缓存数据:

type Node struct {
	key        string
	val        interface{}
	freq       int // 将节点从旧梯队移除时使用,非冗余存储
	prev, next *Node
}

type List struct {
	head, tail *Node
	size       int
}

LFU 操作细节

Set(k, v)

  • 数据已缓存,则更新值,挪到下一梯队
  • 数据未缓存
    • 缓存空间未满:直接挪到第 1 梯队
    • 缓存空间已满:移除 minFreq 梯队的 tail 节点,并将新数据挪到第 1 梯队

Get(k)

  • 命中:节点挪到下一梯队,并返回 value
  • 未命中:返回 -1

如上的 5 种 case,都要维护好对 minFreq 和 2 个哈希表的读写。

代码实现:

type LFUCache struct {
	capacity int
	minFreq  int // 最低频率

	items map[string]*Node
	freqs map[int]*List // 不同频率梯队
}

func NewLFUCache(capacity int) *LFUCache {
	return &LFUCache{
		capacity: capacity,
		minFreq:  0,
		items:    make(map[string]*Node),
		freqs:    make(map[int]*List),
	}
}

func (c *LFUCache) Get(k string) interface{} {
	node, ok := c.items[k]
	if !ok {
		return -1
	}

	// 移到 +1 梯队中
	c.freqs[node.freq].Remove(node)
	node.freq++
	if _, ok := c.freqs[node.freq]; !ok {
		c.freqs[node.freq] = NewList()
	}
	newNode := c.freqs[node.freq].Prepend(node)
	c.items[k] = newNode // 新地址更新到 map
	if c.freqs[c.minFreq].Size() == 0 {
		c.minFreq++ // Get 的正好是当前值
	}
	return newNode.val
}

func (c *LFUCache) Set(k string, v interface{}) {
	if c.capacity <= 0 {
		return
	}

	// 命中,需要更新频率
	if val := c.Get(k); val != -1 {
		c.items[k].val = v // 直接更新值即可
		return
	}

	node := &Node{key: k, val: v, freq: 1}

	// 未命中
	// 缓存已满
	if c.capacity == len(c.items) {
		old := c.freqs[c.minFreq].Tail() // 最低最旧
		c.freqs[c.minFreq].Remove(old)
		delete(c.items, old.key)
	}

	// 缓存未满,放入第 1 梯队
	c.items[k] = node
	if _, ok := c.freqs[1]; !ok {
		c.freqs[1] = NewList()
	}
	c.freqs[1].Prepend(node)
	c.minFreq = 1
}

minFreq 和 2 个哈希表的维护使 LFU 比 LRU 更难实现。

测试

func TestLFU(t *testing.T) {
	c := NewLFUCache(2)
	c.Set(K1, 1)           // 1:K1
	c.Set(K2, 2)           // 1:K2->K1	
	fmt.Println(c.Get(K1)) // 1:K2 2:K1 // 1
	c.Set(K3, 3)           // 1:K3 2:K1
	fmt.Println(c.Get(K2)) // -1
	fmt.Println(c.Get(K3)) // 2:k3->k1  // 3
	c.Set(K4, 4)           // 1:K4 2:K3
	fmt.Println(c.Get(K1)) // -1
	fmt.Println(c.Get(K3)) // 1:K4 3:K3 // 3
}

总结

常见的缓存淘汰策略有队列直接实现的 FIFO,双链表实现的 LFU 与 LRU,此外还有扩展的 2LRU 与 ARC 等算法,它们的实现不依赖于任意一种数据结构,此外对于旧数据的衡量原则不同,淘汰策略也不一样。

在算法直接实现难度较大的情况下,不妨采用空间换时间,或时间换空间的策略来间接实现。要充分利用各种数据结构的优点并互补,比如链表加哈希表就实现了任意操作 O(1) 复杂度的复合数据结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/38427.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ios 通过xib自定义控件

通过xib自定义控件 xib和stroyboayd对比 共同点&#xff1a; 都是用来描述软件界面 都是用interface Builder工具来编辑 本质都是转换成代码去创建控件 不同点&#xff1a; xib是轻量级的&#xff0c;用来描述局部ui界面 创建模型文件 XMGCar 自定义控件 xib 图形设计 …

mac批量提取图片文件名到excel?

mac批量提取图片文件名到excel&#xff1f;最近有个小伙伴向我求助一个电脑操作上的问题&#xff0c;问我在Mac电脑上用什么方法可以快速批量的将大量图片的名称一次性提取出来&#xff0c;并且保存到excel表格里。记得小编曾经给大家分享过windows电脑上批量提取文件名称的方法…

爱心代码李峋同款爱心 python html

目录 前言 一、python 1.python 第一个 2.python第二个 二、HTML 1.第一个 2.第二个html 3.第三个html 3.第四个html 总结 前言 最近那个电视剧很火&#xff0c;就是搞爱心代码的&#xff0c;本人兴趣使然&#xff0c;在网上搜集了一些代码&#xff0c;经过一定修改&…

【iOS】—— 编译链接

【iOS】—— 编译链接 文章目录 【iOS】—— 编译链接编译流程预处理&#xff08;预编译Prepressing&#xff09;编译&#xff08;Compilation&#xff09;汇编&#xff08;Assembly&#xff09;链接&#xff08;Linking&#xff09; 编译流程 编译流程分为四步 预处理&#…

python 安装、配置、使用 xlrd模块、numpy模块

目录 一、xlrd模块 &#xff08;一&#xff09;安装xlrd模块 &#xff08;二&#xff09; pycharm 配置xlrd &#xff08;三&#xff09; 读取xls格式 &#xff08;四&#xff09;xlrd读取时间日期时&#xff0c;会是float类型&#xff0c;需要转换。 二、numpy模块 (一)n…

基于linux下的高并发服务器开发(第一章)- Linux系统IO函数

05 / Linux系统IO函数 &#xff08;1&#xff09;man 2 open >>打开一个已经存在的文件 int open(const char *pathname, int flags); 参数&#xff1a; pathname:要打开文件路径 - flags:对文件的操作权限设置还有其他的设置 O_RDONLY,O_WRONLY,O_RDWR 这三个设置是互斥…

数据结构——各种常见算法的实现方法和思路

文章目录 常见的排序算法类型复杂度和稳定性 1.冒泡排序2.直接插入排序3.希尔排序4.简单选择排序方法1&#xff1a;双向遍历选择排序方法2&#xff1a;单向遍历选择排序 5.归并排序方法1&#xff1a;递归方法2&#xff1a;非递归 6.快速排序方法1&#xff1a;随机取keyi方法2&a…

leetcode 17. 电话号码的字母组合

2023.7.18 该题也是经典回溯题。 与之前做的组合有两点不同&#xff1a; 之前的组合题是求同一集合的组合&#xff0c;而本题是求不同集合的组合。本题还需要有一个将字符串数字转换为手机号9键对应字符集的过程。 下面上代码&#xff1a; class Solution { public:string le…

C# 连接mysql数据库报错:Character set ‘utf8mb3‘ is not supported by .Net Framework.

最近项目突然连接mysql数据库出现一个bug&#xff0c;排查了半小时&#xff0c;最后更新MySql.Data版本解决了&#xff0c;错误信息如下&#xff1a; System.NotSupportedException: Character set utf8mb3 is not supported by .Net Framework.在 MySql.Data.MySqlClient.Cha…

学习PostgreSQL的优势

学习 PostgreSQL 可以为您打开许多就业机会。 PostgreSQL 是一种强大的关系型数据库管理系统&#xff0c;被广泛用于企业和组织中的数据管理和应用程序开发。 以下是一些学习 PostgreSQL 可能帮助您找到的工作领域&#xff1a; **1.数据库管理员&#xff1a;**作为 PostgreSQ…

element 文件批量上传展示上传结果、失败重新上传

效果图&#xff1a; 不废话了&#xff0c;直接上代码&#xff01;&#xff01;&#xff01; HTML部分&#xff1a; <template><div class"container"><el-uploadclass"upload-demo"accept".jpg,.JPG,.png,.PNG"action"#&q…

浅谈物联网在电力行业的应用

摘要&#xff1a;随着社会经济的快速发展&#xff0c;物联网技术也在各个行业中得到了广泛的应用&#xff0c;特别是在电力行业中应用物联网技术&#xff0c;也有效的促进了电力行业的现代化发展。而物联网与智能电网同样都是当代重要的高新技术以及新兴产业。所以通过对于物联…

【论文阅读】《Distilling the Knowledge in a Neural Network》

【论文阅读】《Distilling the Knowledge in a Neural Network》 推荐指数&#xff1a; 1. 动机 &#xff08;1&#xff09;虽然一个ensemble的模型可以提升模型的效果&#xff0c;但是在效率方面实在难以接受&#xff0c;尤其是在每个模型都是一个大型的网络模型的时候。 &…

共筑开源新长城 龙蜥社区走进开放原子校源行-清华大学站

6 月 28 日&#xff0c;以“聚缘于校&#xff0c;开源共行”为主题的 2023 年开放原子校源行活动在清华大学成功举行。本次活动由开放原子开源基金会和清华大学共同主办&#xff0c;来自各行业的 22 位大咖共聚校园共话开源。龙蜥社区技术专家边子政受邀进行技术分享&#xff0…

多元线性回归的梯度下降法

多维特征&#xff08;其实就是从单变量变成了多变量&#xff09; 目前为止&#xff0c;我们探讨了单变量/特征的回归模型&#xff0c;现在我们对房价模型增加更多的特征&#xff0c;例如房间数楼层等&#xff0c;构成一个含有多个变量的模型&#xff0c;模型中的特征为。 增添…

hadoop --- MapReduce

MapReduce定义&#xff1a; MapReduce可以分解为Map (映射) Reduce (规约) &#xff0c; 具体过程&#xff1a; Map : 输入数据集被切分成多个小块&#xff0c;并分配给不同的计算节点进行处理Shuffle and Sort&#xff1a;洗牌和排序&#xff0c;在 Map 阶段结束后&#xf…

Versal ACAP在线升级之Boot Image格式

1、简介 Xilinx FPGA、SOC器件和自适应计算加速平台&#xff08;ACAPs&#xff09;通常由多个硬件和软件二进制文件组成&#xff0c;用于启动这些设备后按照预期设计进行工作。这些二进制文件可以包括FPGA比特流、固件镜像、bootloader引导程序、操作系统和用户选择的应…

使用nginx部署前后端分离项目,处理跨域问题(共享cookie)

1.唠嗑 踩坑了&#xff0c;花费一天时间&#xff0c;开始对nginx配置不懂&#xff0c;老是弄错了配置文件&#xff0c;之前装的nginx ,cofnig有两个&#xff0c;nginx.config和nginx.config.def &#xff0c;开始配置我在nginx.config中配置的&#xff0c;后面一直在改def&…

谈谈VPN是什么、类型、使用场景、工作原理

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 前言 本文将讲解VPN是什么、以及它的类型、使用场景、工作原理。 目录 一、VPN是什么&#xff1f; 二、VPN的类型 1、站点对站点VPN 2、…

「车型分析」控制系统典型应用车型 —— 停车机器人

如今&#xff0c;城市可用土地的日益稀缺&#xff08;城市化&#xff09;和汽车使用数量的增加&#xff08;机动化&#xff09;,为了可持续性发展和其他生活质量问题相结合&#xff0c;由此孕育出来了一种自动停车系统。停车机器人凭借其灵活、高效、标准化的停车模式&#xff…