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 *node
    value myvalue
}

//链表节点的值的结构
type myvalue struct{
    age int
    name string
}

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

节点的值的结构entry

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

type NodeValue interface {
	Len() int
}

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

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

//代表双向链表节点的数据类型
type entry struct {
	key   string
	value NodeValue
}
其整体数据结构
  • 字典的定义是 map[string]*list.Element,键是字符串,值是双向链表中对应节点的指针。
  • maxBytes 是允许使用的最大内存,nbytes 是当前已使用的内存,OnEvicted 是某条记录被移除时的回调函数,可以为 nil。
type Cache struct {
	maxBytes  int64      //允许的能使用的最大内存
	nbytes    int64      //已使用的内存
	ll        *list.List //双向链表
	cache     map[string]*list.Element
	OnEvicted 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] = ele
		c.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 string

func (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

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

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

相关文章

ARM day3

题目&#xff1a;实现3盏灯的流水 代码&#xff1a; .text .global _start _start: 设置RCC寄存器使能 LDR R0,0X50000A28 LDR R1,[R0] ORR R1,R1,#(0X1<<4) ORR R1,R1,#(0X1<<5) STR R1,[R0]设置PE10管脚为输出模式 LDR R0,0X50006000 LDR R1,[R0] BIC R1,R1,…

【动手学深度学习】(十)PyTorch 神经网络基础+GPU

文章目录 一、层和块1.自定义块2.顺序块3.在前向传播函数中执行代码 二、参数管理1.参数访问2.参数初始化3.参数绑定 三、自定义层1.不带参数的层2.带参数的层 四、读写文件1.加载和保存张量2.加载和保存模型参数五、使用GPU [相关总结]state_dict() 一、层和块 为了实现复杂神…

高精度时钟芯片SD2405

概要 SD2405是一款非常优秀的RTC解决方案&#xff0c;为了能让用户在Arduino上有一款方便易用的时钟模块。该模块是一款内置晶振&#xff0c;支持IIC串行接口的高精度时钟模块&#xff1b;内置一次性工业级电池&#xff0c;可保证外部掉电的情况下&#xff0c;可以继续工作5~8…

elk(filebeat)日志收集工具

elk&#xff08;filebeat&#xff09;日志收集工具 elk&#xff1a;filebeat日志收集工具 和logstash相同 filebeat是一个轻量级的日志收集工具&#xff0c;所使用的系统资源比logstash部署和启动时使用的资源要小得多 filebeat可以运行在非Java环境。他可以代理logstash在…

浅谈基于泛在电力物联网的综合能源管控平台设计及硬件选型

贾丽丽 安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;城区内一般都具有错综复杂的能源系统&#xff0c;且大部分能耗都集中于城区的各企、事业单位中。基于泛在电力物联网的综合能源管控平台将城区内从能源产生到能源消耗的整体流动情况采用大屏清晰展示&#xff…

单片机语言--C51语言数据类型与存储类型以及C51的基本运算

单片机语言——C51语言 文章目录 单片机语言——C51语言一、 C51与标准C的比较二、 C51语言中的数据类型与存储类型2.1、C51的扩展数据类型2.2、数据存储类型 三、 C51的基本运算3.1 算术运算符3.2 逻辑运算符3.3 关系运算符3.4 位运算3.5 指针和取地址运算符 一、 C51与标准C的…

深入浅出分析kafka客户端程序设计 ----- 生产者篇----万字总结

前面在深入理解kafka中提到的只是理论上的设计原理&#xff0c; 本篇讲得是基于c语言的kafka库的程序编写&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 首先要编写生产者的代码&#xff0c;得先知道生产者的逻辑在代码上是怎么体现的 1.kafka生产者的逻辑 …

Spark Structured Streaming使用教程

文章目录 1、输入数据源2、输出模式3、sink输出结果4、时间窗口4.1、时间窗口4.2、时间水印&#xff08;Watermarking&#xff09; 5、使用例子 Structured Streaming是一个基于Spark SQL引擎的可扩展和容错流处理引擎&#xff0c;Spark SQL引擎将负责增量和连续地运行它&#…

从主从复制到哨兵模式(含Redis.config配置模板)

文章目录 前言一、主从复制1.概述2.作用3.模拟实践搭建场景模拟实践 二、哨兵模式1.概述2.配置使用3.优缺点4.sentinel.conf完整配置 总结 前言 从主从复制到哨兵模式。 一、主从复制 1.概述 主从复制&#xff0c;是指将一台 Redis 服务器的数据&#xff0c;复制到其他的 Red…

Smart Link和Monitor Link

Smart Link和Monitor Link简介 Smart Link&#xff0c;又叫做备份链路。一个Smart Link由两个接口组成&#xff0c;其中一个接口作为另一个的备份。Smart Link常用于双上行组网&#xff0c;提供可靠高效的备份和快速的切换机制。 Monitor Link是一种接口联动方案&#xff0c;它…

AMEYA360分析兆易创新GD32A490系列车规级MCU

兆易创新GigaDevice今日宣布&#xff0c;正式推出全新GD32A490系列高性能车规级MCU&#xff0c;以高主频、大容量、高集成和高可靠等优势特性紧贴汽车电子开发需求&#xff0c;适用于车窗、雨刷、智能车锁、电动座椅等BCM车身控制系统&#xff0c;以及仪表盘、娱乐影音、中控导…

Google Bard vs. ChatGPT 4.0:文献检索、文献推荐功能对比

在这篇博客中&#xff0c;我们将探讨和比较四个不同的人工智能模型——ChatGPT 3.5、ChatGPT 4.0、ChatGPT 4.0插件和Google Bard。我们将通过三个问题的测试结果来评估它们在处理特定任务时的效能和响应速度。 导航 问题 1: 统计自Vehicle Routing Problem (VRP)第一篇文章发…

Android 11.0 MTK Camera2 设置默认拍照尺寸功能实现

1.前言 在11.0的系统rom定制化开发中,在mtk平台的camera2关于拍照的一些功能修改中,在一些平台默认需要设置最大的分辨率 来作为拍照的分辨率,所以就需要了解拍照尺寸设置流程,然后来实现相关的功能 如图: 2.MTK Camera2 设置默认拍照尺寸功能实现的核心类 \vendor\me…

[数据集][目标检测]拉横幅识别横幅检测数据集VOC+yolo格式1962张1类别

数据集格式&#xff1a;Pascal VOC格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1962 标注数量(xml文件个数)&#xff1a;1962 标注数量(txt文件个数)&#xff1a;1962 标注类别数&a…

一天一个设计模式---原型模式

基本概念 原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;其主要目的是通过复制现有对象来创建新对象&#xff0c;而不是通过实例化类。原型模式允许在运行时动态创建对象&#xff0c;同时避免了耦合与子类化。 在原型模式中&#xff0…

Django模板,Django中间件,ORM操作(pymysql + SQL语句),连接池,session和cookie, 缓存

day04 django进阶-知识点 今日概要&#xff1a; 模板中间件ORM操作&#xff08;pymysql SQL语句&#xff09;session和cookie缓存&#xff08;很多种方式&#xff09; 内容回顾 请求周期 路由系统 最基本路由关系动态路由&#xff08;含正则&#xff09;路由分发不同的app中…

视频相似度对比 python opencv sift flann

提取SIFT特征的代码&#xff0c;返回关键点kp及特征描述符des def SIFT(frame):# 创建SIFT特征提取器sift cv2.xfeatures2d.SIFT_create()# 提取SIFT特征kp, des sift.detectAndCompute(frame, None)return kp, des 这行代码是使用SIFT&#xff08;Scale-Invariant Feature…

Go--协程

协程 协程是Go语言最大的特色之一。 1、协程的概念 协程并不是Go发明的概念&#xff0c;支持协程的变成语言有很多。Go在语言层面直接提供对协程的支持称为goroutine。 1.1 基本概念 进程 进程是应用程序启动的实例&#xff0c;每个进程都有独立的内存空间&#xff0c;不同…

WEB组态编辑器(BY组态)介绍

BY组态是一款非常优秀的纯前端的【web组态插件工具】&#xff0c;可无缝嵌入到vue项目&#xff0c;react项目等&#xff0c;由于是原生js开发&#xff0c;对于前端的集成没有框架的限制。同时由于BY组态只是一个插件&#xff0c;不能独立运行&#xff0c;必须嵌入到你方软件平台…

如何在报表工具 FastReport Cloud 中使用 ClickHouse

FastReport Cloud 是一项云服务 (SaaS)&#xff0c;旨在为您的企业存储、编辑、构建和发送报告。您的整个团队可以从世界任何地方访问这些报告&#xff0c;并且无需创建自己的应用程序。 FastReport Cloud 试用&#xff08;qun&#xff1a;585577353&#xff09;https://chat8.…