缓存淘汰策略

news2024/11/28 2:44:52

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) 复杂度的复合数据结构。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RandLA-Net 复现

GPU3090 CUDA12 1、代码 [github地址] git clone --depth1 https://github.com/QingyongHu/RandLA-Net && cd RandLA-Net 2、虚拟环境中配置&#xff1a; 在跑代码的时候出现错误&#xff1a;open3d.so文件中函数报错。查看open3d版本发现不是要求的0.3版本&#xff…

基于PyQt5的UI界面开发——信号与槽

信号与槽的机制 PyQt5采用了一种被称为“信号与槽”机制的编程模式&#xff0c;用于处理对象间的通信和事件处理。在PyQt5中&#xff0c;信号&#xff08;signal&#xff09;是对象发出的特定事件&#xff0c;例如按钮被点击、文本被修改等。而槽&#xff08;slot&#xff09;…

攻不下dfs不参加比赛(十七)

标题 为什么练dfs题目为什么练dfs 相信学过数据结构的朋友都知道dfs(深度优先搜索)是里面相当重要的一种搜索算法,可能直接说大家感受不到有条件的大家可以去看看一些算法比赛。这些比赛中每一届或多或少都会牵扯到dfs,可能提到dfs大家都知道但是我们为了避免眼高手低有的东…

WooCommerce企业级电子商务需要了解的事情

建立成功的企业业务变得比以往任何时候都容易得多。借助各种可用的平台&#xff0c;将您的想法付诸实践是绝对可行的。 “WooCommerce 是最知名的 WordPress 网站电子商务平台之一。” 它于 2011 年推出&#xff0c;自此受到大型和小型企业的欢迎。它的流行主要归功于其各种免费…

【接口流程分析】唯品会WEB端

唯品会WEB端 来看看唯品会是怎么回事&#xff0c; 地址&#xff1a;aHR0cHM6Ly93d3cudmlwLmNvbS8 https://github.com/Guapisansan/gpss_learn_reverse 代码在这里&#xff0c;会持续更新逆向案例 免责声明&#xff1a; 此文档&#xff0c;以及脚本&#xff0c;仅用来对技术的…

七年老程序员的五六月总结:十一件有意义的事

你好&#xff0c;我是拭心&#xff0c;一名工作七年的安卓开发。 每两个月我会做一次总结&#xff0c;记下这段时间里有意义的事和值得反复看的内容&#xff0c;为的是留一些回忆、评估自己的行为、沉淀有价值的信息。 最近两周的我一直处于“战斗“状态&#xff0c;同时做好…

未来驾驶新标配;CarLuncher车载开发塑造智能娱乐导航系统

车载开发在新能源汽车的快速市场占有率增长背景下具有广阔的前景。随着环境保护意识的增强和政府对清洁能源的支持&#xff0c;新能源汽车&#xff08;如电动汽车&#xff09;在全球范围内呈现出快速增长的趋势。这种趋势为车载开发提供了许多机会和潜在市场。 新能源汽车的普…

一文搞定 Postman 接口自动化测试(全网最全版)

0 前言 本文适合已经掌握 Postman 基本用法的读者&#xff0c;即对接口相关概念有一定了解、已经会使用 Postman 进行模拟请求等基本操作。 工作环境与版本&#xff1a; Window 7&#xff08;64位&#xff09;Postman &#xff08;Chrome App v5.5.3&#xff09; P.S. 不同…

数据结构day3(2023.7.17)

一、Xmind整理&#xff1a; 二、课上练习&#xff1a; 练习1&#xff1a;时间复杂度 时间复杂度&#xff1a;只保留最高阶f(n)3*n^2n^2100nT(n)O(3*n^3n^2100n)O(3*n^3)O(n^3)1>O(1):常数阶int ta; 1ab; 1at; 1f(n)3T(n)O(3)O(3*n^0)O(n^0)O(1)2>O(n): 线性阶for…

selenium:鼠标模拟操作ActionChains

ActionChains 1.导入ActionChains包 from selenium.webdriver import ActionChains 2. 执行原理 调用ActionChains的方法时&#xff0c;不会立即执行&#xff0c;而是将所有的操作,按顺序存放在一个队列里&#xff0c;当你调用perform()方法时&#xff0c;队列中的事件…

EMC学习笔记(十五)射频PCB的EMC设计(二)

射频PCB的EMC设计&#xff08;二&#xff09; 1.滤波1.1 电源和控制线的滤波1.2 频率合成器数据线、时钟线、使能线的滤波 2.接地2.1 接地分类2.2 大面积接地2.3 分组就近接地2.4 射频器件接地2.5 接地时应该注意的问题2.6 接地平面的分布 1.滤波 1.1 电源和控制线的滤波 随着…

项目经理如何处理项目依赖性?

项目不是凭空产生的&#xff0c;项目管理中的依赖性涉及管理和安排项目任务&#xff0c;同时牢记其顺序和要求。如果开始任务B需要先完成任务A&#xff0c;那么可以说任务B依赖于任务A。 这现在听起来可能很简单&#xff0c;但在具有多个相互依赖的任务的复杂项目中&#xff0…

duilib绝对定位与相对定位

文章目录 前言1、绝对位置&#xff08;floattrue&#xff09;2、窗口3、布局及控件4、相对位置&#xff08;floatfalse&#xff09;5、窗口6、布局与控件7、嵌套在布局与控件之中的布局与控件 前言 duilib中窗口&#xff0c;布局&#xff0c;控件等在屏幕上的显示位置都是按照…

Selenium自动化测试-设置元素等待

selenium中有三种时间等待&#xff1a; 强制等待&#xff1a;sleep隐式等待&#xff1a;implicitly_wait显示等待&#xff1a;WebDriverWait 1.sleep 让程序暂停运行一定时间&#xff0c;等待时间到达后继续运行。 使用sleep&#xff0c;需先导入time模块&#xff0c;impor…

一.《某三国》人物属性及其相关属性

人物属性 1.找一个可以操控变化的属性来找 比如血量.坐标或者五铢(绑定金币),这里我们用五铢找 五铢只要打一个怪就会加一点 2.我们直接搜变化即可搜到 五铢地址0AD64EAC 3.我们CE给地址下访问 4.这里我们最后找第一条访问 因为他是被改变的 或者你CE给地址下写入 5.然后我…

BUFG/BUFGCE/BUFH/BUFHCE

对BUFG/BUFGCE/BUFH/BUFHCE简单了解。 下图为 7 系列 FPGA 时钟架构图&#xff1a; BUFG 全局时钟缓冲器。它的输入是IBUFG的输出&#xff0c;BUFG的输出到达FPGA内部的IOB、CLB、选择性块RAM的时钟延迟和抖动最小。BUFG连接的是芯片中的专用时钟资源&#xff0c;能减少信号…

【golang中的切片的相关知识点】[ ] slice

golang-切片 切片的定义和初始化切片的内存分析切片的操作获取长度和容量追加元素复制切片 切片的遍历切片的特性总结 Golang中的切片是一种灵活且强大的数据结构&#xff0c;它可以动态地增长和缩小。切片是基于数组的抽象&#xff0c;它提供了更方便的操作和更灵活的内存管理…

系列五、RocketMQ集群搭建(双主双从)

一、概览 二、集群特点 2.1、NameServer NameServer是一个几乎无状态节点&#xff0c;可集群部署&#xff0c;节点之间无任何信息同步。 2.2、Broker Broker部署相对复杂&#xff0c;Broker分为Master与Slave&#xff0c;一个Master可以对应多个Slave&#xff0c;但是一个Sla…

数字孪生和人工智能异同?

数字孪生和人工智能是两个近年来备受关注的前沿技术&#xff0c;在不同领域发挥着重要作用。虽然两者都涉及数据处理和模拟&#xff0c;但其本质和应用有着显著的区别。本文将介绍数字孪生和人工智能之间的联系和区别&#xff0c;以帮助读者更好地理解它们在不同场景下的作用。…

虚拟机(Ubuntu1804)相机与激光雷达联合标定实现过程记录

在智能小车录制的点云数据在rviz打开一定要修改Fixed Frame为laser_link&#xff0c;这样才能看到点云&#xff0c;注意此时用的是雷神激光雷达&#xff0c;话题名是lslidar_,可采用rostopic list查看具体名称 1、新建一个终端打开roscore roscore2、在文件夹libratia中新建一…