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

相关推荐
JobHu3 分钟前
springboot集成elasticSearch
后端
编程轨迹25 分钟前
剖析 Java 23 特性:深入探究最新功能
后端
洛小豆31 分钟前
一个场景搞明白Reachability Fence,它就像一道“结账前别走”的红外感应门
java·后端·面试
郝同学的测开笔记42 分钟前
云原生探索系列(十六):Go 语言锁机制
后端·云原生·go
七月丶1 小时前
🛠 用 Node.js 和 commander 快速搭建一个 CLI 工具骨架(gix 实战)
前端·后端·github
七月丶1 小时前
🔀 打造更智能的 Git 提交合并命令:gix merge 实战
前端·后端·github
异常君1 小时前
深入剖析 Redis 集群:分布式架构与实现原理全解
redis·分布式·后端
爱上大树的小猪1 小时前
【前端样式】用 aspect-ratio 实现等比容器:视频封面与图片占位的终极解决方案
前端·css·面试
江城开朗的豌豆1 小时前
CSS篇:HTML与XHTML:关键区别与实际应用解析
前端·css·面试
silenceper1 小时前
如何使用Golang开发MCP服务器:从mcp-go到mcp-k8s实践
后端