缓存淘汰策略

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 *Nodesize       int // 缓存空间大小
}

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

func (l *List) Prepend(node *Node) *Node {if l.head == nil {l.head = nodel.tail = node} else {node.prev = nilnode.next = l.headl.head.prev = nodel.head = node}l.size++return node
}func (l *List) Remove(node *Node) *Node {if node == nil {return nil}prev, next := node.prev, node.nextif 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, nilreturn 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]*Nodelist     *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} // 完整的 nodeif 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)) // 100c.Set(K3, 3)fmt.Println(c.Get(K2)) // -1c.Set(K4, 4)fmt.Println(c.Get(K1)) // -1fmt.Println(c.Get(K3)) // 3fmt.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        stringval        interface{}freq       int // 将节点从旧梯队移除时使用,非冗余存储prev, next *Node
}type List struct {head, tail *Nodesize       int
}

LFU 操作细节

Set(k, v)

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

Get(k)

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

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

代码实现:

type LFUCache struct {capacity intminFreq  int // 最低频率items map[string]*Nodefreqs 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 // 新地址更新到 mapif 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] = nodeif _, 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:K1c.Set(K2, 2)           // 1:K2->K1	fmt.Println(c.Get(K1)) // 1:K2 2:K1 // 1c.Set(K3, 3)           // 1:K3 2:K1fmt.Println(c.Get(K2)) // -1fmt.Println(c.Get(K3)) // 2:k3->k1  // 3c.Set(K4, 4)           // 1:K4 2:K3fmt.Println(c.Get(K1)) // -1fmt.Println(c.Get(K3)) // 1:K4 3:K3 // 3
}

总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【电路原理学习笔记】第4章:能量与功率:4.4 能量转换与电阻的电压降

第4章&#xff1a;能量与功率 4.4 能量转换与电阻的电压降 图4-11以电子作为电荷进行举例说明&#xff0c;电子从电池的负极流过电路&#xff0c;然后流回正极。当它们从负极出来时&#xff0c;电子具有最高的能量水平。电子流经过每个电阻&#xff0c;这些电阻连接在一起形成…

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

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

c++基础语法

c基础语法 c的关键字 asm 、do 、if、 return、 try、 continue、 auto、 double、 inline、 short、 typedef、 for、 bool、 dynamic_cast、 int、 signed 、typeid、 public、 break 、else 、long 、sizeof 、typename 、throw、 case、 enum 、mutable、 static、 union、…

C#(六十二)之泛型的约束

类型约束 基类约束有两个重要的目的。 1&#xff1a;它允许在泛型类中使用有约束指定的基类成员。 2&#xff1a;确保只能使用支持指定基类或派生类的类型实例。 约束是使用 where 上下文关键字指定的。 下表列出了五种类型的约束&#xff1a; 约束 说明 T&#xff1a;str…

SpringCloud集成Sleuth+Zipkin进行链路追踪

关于微服务链路追踪的一些概念&#xff1a;【分布式链路追踪】 文章目录 1、整合Seluth2、日志信息分析3、Zipkin介绍4、Zipkin服务端安装5、搭配Sleuth整合客户端Zipkin6、收集数据7、存储trace数据 1、整合Seluth Spring Cloud Sleuth是一个用于追踪的工具&#xff0c;它可以…

智能化升级:科智立RFID AGV地标传感器助力AGV小车升级改造

引言 在这个日新月异的信息化时代&#xff0c;各行各业都在不断进行升级改造&#xff0c;以求在激烈的市场竞争中立于不败之地。其中&#xff0c;智能化升级是提升企业核心竞争力的重要手段。本篇文章将以AGV小车的智能化升级为话题&#xff0c;深入探讨科智立RFID AGV地标传感…

从0到1:跑团小程序开发心得笔记

背景介绍 随着健康意识的兴起&#xff0c;越来越多的人选择加入跑步俱乐部&#xff0c;不仅体验到了运动的乐趣&#xff0c;也感受到了人生的不同色&#xff0c;那么通过小程序&#xff0c;把俱乐部搬到手机上&#xff0c;通过小程序了解俱乐部动态和运动常识&#xff0c;可以…

网络安全问题严重?Web3和IP代理能否提供解决方案?

在数字世界中&#xff0c;网络安全问题越来越严重。黑客和网络犯罪分子利用各种手段来窃取个人信息、盗取数字资产或破坏网络服务。为了应对这些威胁&#xff0c;Web3和IP代理成为了有力的工具&#xff0c;可以帮助改善网络安全性&#xff0c;提高网络防御水平。 首先&#xff…

智能会议记录:利用剪映和Claude实现高效会议纪要制作

具体操作步骤可以是: 1、使用录音笔或手机录音将会议过程录制下来。 2、将录音文件上传到pc端&#xff0c;使用剪映程序,利用语音转文本和自然语言处理技术,分析音频&#xff0c;生成文字稿。 3、打开Claude网站。 4、粘贴会议文字。 5、输入提示词&#xff1a; 用中文总结该…

office CVE-2022-30190 RCE 复现

简介: 当用户点击word等应用程序时&#xff0c;会使用URL协议调用MSDT,随即启动msdt.exe程序造成远程代码执行漏洞。简单来说就是攻击者可以恶意构造一个word文档&#xff0c;诱骗受害者点击&#xff0c;从而导致系统被黑。 0x01 环境部署 1. 测试版本 Microsoft Office LTSC …

cjson坑系列二cJSON_AddStringToObject、cJSON_AddItemToObject申请内存失败

在使用cjson的时候遇见这样一个问题&#xff08;先看问题代码&#xff0c;如下&#xff09; void platform_set_matrix_by_udp(cJSON* para, const char* str) {char* chintstr NULL, * intstr NULL;cJSON* index, * val;int ch;char* matrix;int number 0;int rows[8] { …

arcgis建筑密度容积率覆盖率

大多数研究会把覆盖率当作建筑密度。 用覆盖率更恰当一些。 建筑覆盖率&#xff1a; 建筑物占据面积/街区面积 [Sum_area]/ ([area]*1000000) 排序检查数据&#xff0c;最大值0.75&#xff0c;最小值0. 建筑覆盖率&#xff0c;建筑密度的范围都应该在0-1之内&#xff0c;不是…