cache教程1.LRU 缓存淘汰策略

这一节实现LRU算法,要理解明白其使用的数据结构。

FIFO/LFU/LRU 算法简介

Cache的缓存全部存储在内存中,内存是有限的,因此不可能无限制地添加数据。当占用内存超过了给定的内存大小时候,就需要从缓存中移除一条或多条数据了。我们肯定希望尽可能移除“没用”的数据,那如何判定数据“有用”还是“没用”呢?

FIFO(First In First Out)

先进先出,也就是淘汰缓存中最老(最早添加)的记录。这种算法的实现简单,创建一个队列,新增记录添加到队尾,每次内存不够时,淘汰队首。这样对那些最早添加的却需要频繁访问的数据很不友好。

LFU(Least Frequently Used)

最少使用,也就是淘汰缓存中访问频率最低的记录。其实现需要维护一个按照访问次数排序的队列,每次访问,访问次数加1,队列重新排序,淘汰时选择访问次数最少的即可。LFU 算法的命中率是比较高的,但缺点也非常明显,维护每个记录的访问次数,对内存的消耗是很高的;另外,如果数据的访问模式发生变化,LFU 需要较长的时间去适应,也就是说 LFU 算法受历史数据的影响比较大。

LRU(Least Recently Used)

最近最少使用,其算法可以认为是相对平衡的一种淘汰算法。LRU 算法的实现非常简单,维护一个队列,如果某条记录被访问了,则移动到队首,那么队尾则是最近最少访问的数据,淘汰该条记录即可。

LRU 算法实现

核心数据结构

 图片来自极客兔兔

 这是字典(map)和双向链表

为什么是map和双向链表这两个数据结构?

map很容易理解,存储键和值的映射关系。这样根据某个键(key)查找对应的值(value)的复杂是O(1),在字典中插入一条记录的复杂度也是O(1)

讲解该算法时,说的是维护一个队列,但为什么就使用链表呢?

因为队列只能操作队头和队尾的数据,要进行删除,可能删除的是中间部分的数据,所以就不能使用队列。就可以用链表来实现符合我们要求的队列。

而为什么使用双向链表,而不用单向链表呢?

首先增加或删除的操作的复杂度均是O(1)才符合我们的要求。

使用单向链表的时候,在中间部分删除链表节点时候,其时间复杂度不是O(1)。删除给定节点,那就需要找到给定节点的前一个节点,只有双向链表才保存了前一个节点。而且使用双向链表也方便快速把数据放在队首/队尾。所以使用双向链表。

这里使用Go中container/list的双向链表。其使用和c++的稍微不同,c++中声明链表是std::list<int>,要注明元素类型的。而Go中是不需要的,而且其元素的值是any泛型。每个元素的值的类型都可以不同。

func main() {ll := list.New()ll.PushFront(1)ll.PushFront("home")fmt.Println(ll.Front().Value.(string))fmt.Println(ll.Back().Value.(int))//下面的写法也成功打印出来// fmt.Println(ll.Front().Value)// fmt.Println(ll.Back().Value)
}
//打印结果
home
1

具体实现

使用链表的话,就要确定双向链表节点的数据结构节点的值的数据类型。例如:

//链表节点的结构
type node struct{next *nodevalue myvalue
}//链表节点的值的结构
type myvalue struct{age intname string
}

双向链表节点的数据结构是已经确定的了,因为使用了Go中自带的list,其节点结构是Element。确定了节点结构,那就要确定该节点的值的结构。

节点的值的结构entry

 因为我们要实时统计存储大小, 所以存储的元素都要能够计算大小。所以我们抽象出如下接口NodeValue:

type NodeValue interface {Len() int
}

为了通用性,我们允许值是实现了 NodeValue 接口的任意类型,该接口只包含了一个方法 Len() int,用于返回值所占用的内存大小。

我们定义结构体entry。键值对 entry 是双向链表节点的值的数据类型,在链表中仍保存每个值对应的 key 的好处在于,淘汰队尾节点时,需要用 key 从字典中删除对应的映射。

//代表双向链表节点的数据类型
type entry struct {key   stringvalue NodeValue
}
其整体数据结构
  • 字典的定义是 map[string]*list.Element,键是字符串,值是双向链表中对应节点的指针。
  • maxBytes 是允许使用的最大内存,nbytes 是当前已使用的内存,OnEvicted 是某条记录被移除时的回调函数,可以为 nil。
type Cache struct {maxBytes  int64      //允许的能使用的最大内存nbytes    int64      //已使用的内存ll        *list.List //双向链表cache     map[string]*list.ElementOnEvicted func(key string, value NodeValue)
}//list节点Element结构体源码
// Element is an element of a linked list.
type Element struct {next, prev *Element// The list to which this element belongs.list *List// The value stored with this element.Value any
}

list源码的Element结构体中的Value是any泛型,可以存储我们之前定义的*entry类型。

链表的结构就如下图所示:Element结构体内的Value变量类型就是*entry。

定义初始化缓存函数
func New(maxBytes int64, onEvicted func(string, Value)) *Cache {return &Cache{maxBytes:  maxBytes,ll:        list.New(),cache:     make(map[string]*list.Element),OnEvicted: onEvicted,}
}
定义添加/修改功能
func (c *Cache) Add(key string, value NodeValue) {if ele, ok := c.cache[key]; ok {c.ll.MoveToFront(ele)kv := ele.Value.(*entry)c.nbytes += int64(value.Len()) - int64(kv.value.Len())kv.value = value} else { //还没存在该数据的情况ele := c.ll.PushFront(&entry{key, value})c.cache[key] = elec.nbytes += int64(len(key)) + int64(value.Len())}for c.maxBytes != 0 && c.maxBytes < c.nbytes {c.RemoveOldest()}
}

主要是三步

  • 如果键存在,则更新对应节点的值,并将该节点移到队尾。
  • 不存在则是新增场景,首先队尾添加新节点 &entry{key, value}, 并在字典中添加 key 和该节点的映射关系。
  • 更新 c.nbytes,如果超过了设定的最大值 c.maxBytes,则移除最少访问的节点。

第四行的ele.Value.(*entry)是类型转换的意思。ele.Value是any泛型,需要转换成*entry类型。

Go语言中使用接口断言(type assertions)将接口转换成另外一个接口,也可以将接口转换为另外的类型。类型断言是一个使用在接口值上的操作。语法上它看起来像 i.(T) 被称为断言类型,这里 i 表示一个接口的类型和 T 表示一个类型。

t := i.(T)
定义查找功能

主要有 2 个步骤,第一步是从字典中找到对应的双向链表的节点,第二步,将该节点移动到队首。

func (c *Cache) Get(key string) (v NodeValue, ok bool) {if ele, ok := c.cache[key]; ok {c.ll.MoveToFront(ele) //取到该元素,其成为了热点数据, 所以要往队首放kv := ele.Value.(*entry)return kv.value, true}return
}
删除功能

实际上是缓存淘汰。即移除最近最少访问的节点(队尾)。

func (c *Cache) RemoveOldest() {ele := c.ll.Back()if ele == nil {return}c.ll.Remove(ele)kv := ele.Value.(*entry)delete(c.cache, kv.key) //从map中删除c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len())if c.OnEvicted != nil {c.OnEvicted(kv.key, kv.value) //执行被删除时的回调函数}
}

为了方便测试,我们实现 Len() 用来获取添加了多少条数据。

// Len the number of cache entries
func (c *Cache) Len() int {return c.ll.Len()
}

测试

尝试添加几条数据,测试 Get 方法。go test -run TestGet

//该类型实现了NodeValue接口
type String stringfunc (d String) Len() int {return len(d)
}func TestGet(t *testing.T) {lru := New(0, nil)lru.Add("key1", String("abc"))//String(v.(String)), v是NodeValue接口类型,需要进行类型转换if v, ok := lru.Get("key1"); !ok || String(v.(String)) != "abc" {t.Fatalf("cache hit key1=1234 failed")}if _, ok := lru.Get("key2"); ok {t.Fatalf("cache miss key2 failed")}
}

测试,当使用内存超过了设定值时,是否会触发“无用”节点的移除:

go test -run TestRemoveoldest

func TestRemoveoldest(t *testing.T) {k1, k2, k3 := "k1", "k2", "k3"v1, v2, v3 := "v1", "v2", "v3"cap := len(k1 + k2 + v1 + v2)lru := New(int64(cap), nil)lru.Add(k1, String(v1))lru.Add(k2, String(v2))lru.Add(k3, String(v3))//容量只够k1和k2,现在也添加了k3,那需要把k1删除if _, ok := lru.Get("k1"); ok || lru.Len() != 2 {t.Fatalf("Removeoldest key1 failed")}
}

完整代码:https://github.com/liwook/Go-projects/tree/main/go-cache/1-lru

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

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

相关文章

【机器学习】042_迁移学习

一、概述、定义 目的&#xff1a; 迁移学习的目的是将某个领域或任务上学习到的模式、知识应用到不同但相关的领域里&#xff0c;获取更多数据&#xff0c;而不必投入许多时间人力来进行数据的标注。 举例&#xff1a; 已经会下中国象棋&#xff0c;就可以类比着来学习国际…

shell脚本实战之mysql-物理备份实现周内备份策略

一.shell脚本要求 # 数据库备份脚本方式: 物理备份# 工具:xtrabackup过程# 需求1:不允许有任何的安在脚本中&#xff0c;# 需求2:在脚本中&#xff0c;不允许有任何的交互式代码容&#xff0c;使用日志记录# 需求3:在脚本中&#xff0c;不允许有任何输出# 需求4:每周为一个循环…

SSD数据在写入NAND之前为何要随机化?-part2

接part1介绍&#xff1a; 如何达到这个目的&#xff1f;业内常用的是对写入数据的数据进行随机化处理&#xff0c;这部分主要在SSD控制器中通过硬件实现。 上图b/c&#xff1a;在控制器芯片通过硬件方式实现随机化的读写流程&#xff0c;这个也是业内通常做法。随机化处理是在写…

JVM虚拟机:命令行查看JVM垃圾回收器的执行信息

在eclipse中打开命令行窗口 window->show view->Terminal 这样就打开了Terminal窗口&#xff0c;效果如下所示&#xff1a; java -XX:PrintCommandLineFlags -version 这个命令可以查看一些配置信息&#xff0c;其中最重要的配置信息就是&#xff0c;当前使用的G1回收器…

机器学习算法性能评估常用指标总结

考虑一个二分问题&#xff0c;即将实例分成正类&#xff08;positive&#xff09;或负类&#xff08;negative&#xff09;。对一个二分问题来说&#xff0c;会出现四种情况。如果一个实例是正类并且也被 预测成正类&#xff0c;即为真正类&#xff08;True positive&#xff0…

Java到底是什么?学了我们能做什么?

一、Java是什么&#xff1f; Java是一门面向对象编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表&#xff0c;极好地实…

软件设计中如何画各类图之九活动图:系统行为与流程的可视化描述

目录 1 前言2 活动图的符号及说明2.1 开始和结束节点2.2 活动&#xff08;Action&#xff09;2.3 控制流&#xff08;Control Flow&#xff09;2.4 决策节点&#xff08;Decision Node&#xff09;2.5 合并节点&#xff08;Merge Node&#xff09; 3 画活动图的步骤3.1 确定活动…

Vue3拖拽功能【VueDraggablePlus】

一、介绍 由于 Sortablejs 的 vue3 组件一直没有更新&#xff0c;已经跟 vue3 严重脱节&#xff0c;所以诞生了这个项目&#xff0c;这个组件是基于 Sortablejs 的&#xff0c;所以如果你想了解更多关于 Sortablejs 的信息&#xff0c;可以查看Sortablejs 官网。VueDraggableP…

WIFI直连(Wi-Fi P2P)

一、概述 Wifi peer-to-peer&#xff08;也称Wifi-Direct&#xff09;是Wifi联盟推出的一项基于原来WIfi技术的可以让设备与设备间直接连接的技术&#xff0c;使用户不需要借助局域网或者AP&#xff08;Access Point&#xff09;就可以进行一对一或一对多通信。这种技术的应用…

微表情检测(三)----基于光流特征的微表情检测

Micro-expression spotting based on optical flow features 基于光流特征的微表情检测 Abstract 本文提出了一种高精度和可解释性的自动微表情检测方法。首先&#xff0c;我们设计了基于鼻尖位置的图像对齐方法&#xff0c;以消除由头部晃动引起的全局位移。其次&#xff0…

C语言----文件操作(一)

一&#xff1a;C语言中文件的概念 对于文件想必大家都很熟悉&#xff0c;无论在windows上还是Linux中&#xff0c;我们用文件去存储资料&#xff0c;记录笔记&#xff0c;常见的如txt文件&#xff0c;word文档&#xff0c;log文件等。那么&#xff0c;在C语言中文件是什么样的存…

【Hung-Yi Lee】强化学习笔记

文章目录 What is RLPolicy GradientPolicy Gradient实际是怎么做的On-policy v.s. Off-policyExploration配音大师 Actor-Critic训练value function的方式网络设计DQN Reward ShapingNo Reward&#xff1a;Learning from Demonstration What is RL 定义一个策略网络&#xff0…