使用 go 语言实现一个 LRU 缓存算法

LRU(Least Recently Used,最近最少使用)缓存是一种常见的缓存策略,经典的实现方式是结合哈希表和双向链表:哈希表用于快速查找缓存项,双向链表用于维护缓存项的访问顺序。那我们要如何用代码实现呢,之前我在深入解析Go语言container/list:双向链表的实现与应用中介绍了go语言中的双向链表,这可以减少我们对链表的实现,这里只需实现一个LRUCache结构体,用于存放哈希表和双向链表。


LRU 的读写时序图

在开始代码编写前,先简单说下以 LRU 策略的缓存读写的流程:

读场景

  • 首先用户先从缓存中查找
  • 缓存命中的话,需要将命中的数据放到链表的头部再返回值
  • 如果不存在就返回一个不存在的标识(例如 -1)

写场景

  • 用户往缓存中插入一个键值对,首先查看缓存中是否存在
  • 如果存在,更新值,且将元素移到链表头部
  • 如果不存在,则先将数据插入到链表头部,如何将键值对插入到缓存中
  • 插入缓存的过程需要判断当前缓存的大小是否超过容量
  • 如果超过,则移除链表尾部的元素,并将其在缓存中删除

具体的流程如下时序图所示:


有了上面简单的方案,我们接下来只需按照时序图实现相关的操作即可。

1. 定义缓存结构

创建一个结构体来表示 LRU 缓存,包含缓存容量、用于存储键值对的哈希表以及用于记录使用顺序的双向链表。

go 复制代码
import (
	"container/list"
)

type LRUCache struct {
	capacity int 
	cache    map[int]*list.Element 
	lruList  *list.List
}

capacity 表示缓存的最大容量,cache 是一个哈希表,用于快速查找缓存项,lruList 是一个双向链表,用于记录缓存项的使用顺序。

2. 实现初始化方法

实现一个初始化方法,用于创建一个新的 LRU 缓存实例。

go 复制代码
func Constructor(capacity int) LRUCache {
	return LRUCache{
		capacity: capacity,
		cache:    make(map[int]*list.Element),
		lruList:  list.New(),
	}
}

3. 实现 Get 方法

Get 方法用于从缓存中获取指定键的值。如果键存在于缓存中,就将对应的缓存项移到链表头部(表示最近被使用),并返回其值;如果键不存在,就返回 -1。

go 复制代码
func (l *LRUCache) Get(key int) int {
	if elem, ok := l.cache[key]; ok {
		// 将访问的元素移到链表头部
		l.lruList.MoveToFront(elem)
		return elem.Value.(*list.Element).Value.(int)
	}
	return -1
}

4. 实现 Put 方法

Put 方法用于将键值对添加到缓存中。如果键已经存在,就更新其值,并将对应的缓存项移到链表头部;如果键不存在,就添加一个新的缓存项到链表头部,并将键值对添加到哈希表中。如果缓存已满,则需要移除链表尾部的缓存项(最久未使用的)。

go 复制代码
func (l *LRUCache) Put(key int, value int) {
	if elem, ok := l.cache[key]; ok {
		// 更新值
		lruElem := elem.Value.(*list.Element)
		lruElem.Value = value
		// 将访问的元素移到链表头部
		l.lruList.MoveToFront(elem)
	} else {
		// 添加新的元素到链表头部
		elem := l.lruList.PushFront(&list.Element{Value: value})
		l.cache[key] = elem
		// 如果缓存已满,移除链表尾部的元素
		if len(l.cache) > l.capacity {
			backElem := l.lruList.Back()
			delete(l.cache, backElem.Value.(*list.Element).Value.(int))
			l.lruList.Remove(backElem)
		}
	}
}

5. 使用示例

go 复制代码
func main() {
	cache := Constructor(2) // 创建一个容量为 2 的缓存
	cache.Put(1, 1)
	cache.Put(2, 2)
	fmt.Println(cache.Get(1)) // 返回 1
	cache.Put(3, 3)           // 缓存已满,移除键 2
	fmt.Println(cache.Get(2)) // 返回 -1 (不命中)
	cache.Put(4, 4)           // 缓存已满,移除键 1
	fmt.Println(cache.Get(1)) // 返回 -1 (不命中)
	fmt.Println(cache.Get(3)) // 返回 3
	fmt.Println(cache.Get(4)) // 返回 4
}

以上就是用 Go 语言实现 LRU 缓存的一个简单示例。

相关推荐
寻求出路的程序媛4 小时前
分布式 & 高性能 & 高可用 体系、学习重点、面试点
分布式·后端·面试·性能优化
掘金者阿豪4 小时前
Let‘s Encrypt 证书到底会不会自动续期?从一次服务器迁移后的证书排查说起
后端
课件帮4 小时前
教师考编面试模拟授课课件怎么做?课件帮AI生成教案+PPT+数字人试讲
人工智能·面试·powerpoint
RisunJan4 小时前
项目经理面试知识点梳理
面试·职场和发展·word
joinwell524 小时前
Agent 中断后,原任务如何安全接管?从恢复标记到效果事实
人工智能·后端·架构
sp424 小时前
羽量级的 Java Bean 实体校验器
后端
魔兽大山哥4 小时前
【NL2SQL 实战 07】LIMIT 不是小事:默认 100 行背后的产品判断
后端
qq_339191144 小时前
go cpu占比高排查,cpu100%排查,go pprof cpu命令
开发语言·后端·golang
笃行3504 小时前
异构数据同步如何真正做到“数据无忧“?——解读 KFS 的全周期一致性校验与修复能力
后端
云边有个稻草人4 小时前
异构数据同步如何保证数据不出错:KFS全周期校验与修复实践
后端