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

相关推荐
撸猫79131 分钟前
HttpSession 的运行原理
前端·后端·cookie·httpsession
嘵奇1 小时前
Spring Boot中HTTP连接池的配置与优化实践
spring boot·后端·http
子燕若水1 小时前
Flask 调试的时候进入main函数两次
后端·python·flask
程序员爱钓鱼1 小时前
跳转语句:break、continue、goto -《Go语言实战指南》
开发语言·后端·golang·go1.19
chenyuhao20242 小时前
链表的面试题4之合并有序链表
数据结构·链表·面试·c#
Persistence___2 小时前
SpringBoot中的拦截器
java·spring boot·后端
嘵奇2 小时前
Spring Boot 跨域问题全解:原理、解决方案与最佳实践
java·spring boot·后端
一丝晨光4 小时前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
景天科技苑4 小时前
【Rust泛型】Rust泛型使用详解与应用场景
开发语言·后端·rust·泛型·rust泛型
PgSheep6 小时前
深入理解 JVM:StackOverFlow、OOM 与 GC overhead limit exceeded 的本质剖析及 Stack 与 Heap 的差异
jvm·面试