使用 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 缓存的一个简单示例。

相关推荐
zyplayer-doc4 小时前
VuePress类静态文档站和动态知识库怎么选:两种技术路线的适用场景
javascript·人工智能·后端·安全·智能手机
rannn_1114 小时前
【力扣hot100】链表专题|160、206、234、141、142
java·算法·leetcode·链表·面试·开发
用户938515635076 小时前
从前后端分离到前端接口工程:React + MockJS + Vite 解析
前端·后端·全栈
码事漫谈7 小时前
世界由什么构成——这个问题比你想的更缠人
后端
摇滚侠7 小时前
《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 1
spring boot·笔记·后端
山荷枝7 小时前
03-框架--Spring
java·后端·spring
angryshan7 小时前
goLang配置断点流程
go·bug
ChaHae-In8 小时前
SpringBoot统一功能处理
java·spring boot·后端