Golang实现带图解的简易版过期LRU缓存
笔记哥 /
04-22 /
42点赞 /
0评论 /
740阅读
# 1、支持Put、Get的LRU实现
想要实现一个带过期时间的LRU,从易到难,我们需要先学会如何实现一个普通的LRU,做到O(1)的Get、Put。
想要做到O(1)的Get,我们很容易想到**使用哈希表来存储每个key对应的value**;要想实现O(1)的Put,并且能当容量满了的时候自动弹出最久未使用的元素,单纯使用哈希表是比较难实现的,因此我们可以使用一个**双向链表**,**头部存放最新被使用的节点**,**尾部存放最久未使用的节点**。那么哈希表只需要**记录key到node的映射**,就能让我们快速的追踪到节点在双向链表中的位置。
为了使得双向链表易于维护,我们可以增加**两个哨兵节点**,分别**代表头部和尾部**。
到这里,我们就能确定实现一个简单操作的LRU需要涉及的数据结构:**双向链表、哈希表**。

## 1.1、数据结构定义
现在,我们将**基本的数据结构**定义出来,并且**定义个构造器函数**。
```csharp
type Node struct {Next, Pre *Nodekey, val int
}
type DoubleLinkList struct {Head *NodeTail *Node
}
type LRUcache struct {doubleList DoubleLinkListKeyToNode map[int]*Nodecap, cnt int
}
func Constructor(cap int) *LRUcache {head := &Node{}tail := &Node{}head.Next = tailtail.Pre = headlru := &LRUcache{ doubleList: DoubleLinkList{ Head: head, Tail: tail, }, cap: cap, KeyToNode: make(map[int]*Node, cap),}return lru
}
```
## 1.2、方法分析
我们可以先来考虑Put方法。当Put一个(key,value)对的时候,需要先检查是否存在对应的key,若存在,我们就只需要**更新这个节点的值并且将节点移动至头部就可以了**;若不存在,就需要创建一个节点,添加到头部,若LRUcache满了,就需要**移除最久没使用的尾部节点**。
再来考虑Get方法,假若我们能获取到key对应的节点,那么就需要将这个节点**更新至链表的头部**,然后返回值即可;否则,直接返回-1.
从上面的分析我们不难发现,实现Get和Put方法,需要实现三个基本操作:**移除一个节点、移除尾部节点、添加节点至头部**。
上面的方法,涉及到哈希表的值改变的,也需要一一处理。
接下来我们一一实现。
## 1.3、添加节点至头部
```csharp
func (c *LRUcache) AddNode(node *Node) {//哈希表注册这个节点c.KeyToNode[node.key] = node//添加到链表中,头节点之后node.Next = c.doubleList.Head.Nextnode.Pre = c.doubleList.Headc.doubleList.Head.Next.Pre = nodec.doubleList.Head.Next = node//更新表记录节点数c.cnt++
}
```
执行过程:
- 注册该节点到哈希表中
- 将该节点的前后指针正确设置
- 将原本的第一个节点的Pre指针设置为node
- 将头节点的Next设置为node
- 更新cnt计数器
## 1.4、移除节点
```csharp
func (c *LRUcache) RemoveNode(node *Node) {//哈希表删除节点记录delete(c.KeyToNode, node.key)//更新链表node.Next.Pre = node.Prenode.Pre.Next = node.Next//更新计数器c.cnt--
}
```
## 1.5、移除尾部节点
```csharp
func (c *LRUcache) RemoveTail() {//获取尾部节点node := c.doubleList.Tail.Pre//移除c.RemoveNode(node)
}
```
## 1.6、Get()
接下来,就可以实现Get和Put方法了。先来实现一下Get
```csharp
func (c *LRUcache) Get(key int) int{//查询节点是否存在if node, ok := c.KeyToNode[key]; ok{ //存在:从链表移除、添加到链表头部 c.RemoveNode(node) c.AddNode(node) return node.val}else{ //不存在,返回-1 return -1}
}
```
## 1.7、Put()
Put的执行流程也比较简单:
```csharp
func (c *LRUcache) Put(key int, val int) {//检查节点是否存在if node, ok := c.KeyToNode[key]; ok { //存在:更新节点的值、移除节点、添加节点到头部 node.val = val c.RemoveNode(node) c.AddNode(node)} else { //不存在,先检查容量是否上限 if c.cnt == c.cap { c.RemoveTail() //移除尾部节点 } //容量足够,添加节点 newNode := &Node{key: key, val: val} c.AddNode(newNode)}
}
```
至此,一个简单的LRU就实现了。
# 2、优雅实现带过期时间的LRU
现在,我们来考虑如何引入过期时间。
首先,我们当然要为每个node添加一个过期时间字段,来记录它什么时候会过期。
对于用户来说,为了保证数据是有效的,每次Get一个值,是**不允许用户获取到一个过期对象的**。这里可以采用一个**懒更新**的策略,当调用Get获取到一个节点的时候,应该**检查是否已经过期**,过期了就应该去除掉这个节点,给用户返回-1。
这样子,我们就已经对用户的值获取有了个最基本的保障,至少不会获取到已经过期的数据。接下来我们就要考虑,如何去**实现节点的自动过期删除呢**?
要快速的**获取到最早要过期的节点**,我们可以引入一个**过期时间最小堆**,位于堆顶的是最早将要过期的节点;然后为了实现**“自动管理”**,我们可以**引入一个协程去打理这个堆**,每次发现节点过期了,就让这个协程自己去把节点清理掉就好了。这样子,可以做到当有节点过期了,只需要`O(logn)`的时间去清理掉节点,Put/Get主流程仍然只需要O(1)的时间复杂度,做到了“优雅高效”。
以为我们引入了协程,这就会有线程安全的问题,所以需要对LRU**添加一把锁**,来实现对操作的安全访问。
接着,我们希望当LRU存在节点的时候,再进行检查是否过期,为了**防止协程长期自旋检查**,我们可以为**LRUcache添加一个sycn.Cond**,**做到当没有节点的时候,就可以沉睡等待被唤醒**。(一个优化的点)

接下来,我们一一修改时间。
## 2.1、数据结构修改
原本的node需要添加过期时间的记录,以及为了实现最小堆,需要添加index下标,记录在堆的位置。
```csharp
type Node struct {Next, Pre *Nodekey, val intindex intexpire time.Time
}
```
接着是LRUcache,添加一个最小堆结构,然后还需要添加锁,以及sync.Cond。
附带实现这个最小堆(使用container/heap)
```csharp
// 最小堆实现
type MinHeap []*Node
func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Swap(i, j int) {h[i], h[j] = h[j], h[i]h[i].index, h[j].index = i, j
}
func (h *MinHeap) Push(x interface{}) {n := h.Len()node := x.(*Node)node.index = n*h = append(*h, node)
}
func (h *MinHeap) Pop() interface{} {old := *hn := old.Len()node := old[n-1]node.index = -1*h = old[:n-1]return node
}
```
然后是结构的修改,和构造器修改
```csharp
type LRUcache struct {doubleList DoubleLinkListKeyToNode map[int]*Nodecap, cnt int
expHeap MinHeapmu sync.MutexexpCond sync.Cond
}
```
```csharp
func Constructor(cap int) *LRUcache {head := &Node{}tail := &Node{}head.Next = tailtail.Pre = headlru := &LRUcache{ doubleList: DoubleLinkList{ Head: head, Tail: tail, }, cap: cap, KeyToNode: make(map[int]*Node, cap),}heap.Init(&lru.expHeap)//初始化堆lru.expCond = *sync.NewCond(&lru.mu)//创建condgo lru.expirer()return lru
}
```
## 2.2、清理协程实现
```csharp
func (c *LRUcache) expirer() {for { //获取锁 c.mu.Lock() //若列表没有节点,就沉睡等到被put唤醒 for c.expHeap.Len() == 0 { c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。 } //获取堆顶节点 node := c.expHeap[0] now := time.Now() wait := node.expire.Sub(now) //如果wait>0,说明还没有过期 if wait > 0 { //沉睡到该节点过期 c.mu.Unlock() time.Sleep(wait) //醒来后,要重新执行一次流程 continue } //清理这个节点 heap.Pop(&c.expHeap) c.RemoveNode(node) c.mu.Unlock()}
}
```
流程:
- 获取锁
- 检查队列是否为空,为空则等待被put唤醒
- 获取堆顶节点,检查是否已经到达过期时间
- 未到达过期时间,则沉睡,当被唤醒的时候,重新检查堆顶。
- 到达了过期时间,进行清除操作
## 2.3、Get修改
现在引入了过期机制后,就需要检查是否过期,以及获取锁才能操作。
```csharp
func (c *LRUcache) Get(key int) int {//修改1c.mu.Lock()defer c.mu.Unlock()//查询节点是否存在if node, ok := c.KeyToNode[key]; ok { //修改2,检查是否过期 if time.Now().After(node.expire) { //过期了,协程还没有发现,手动帮助删除 heap.Remove(&c.expHeap, node.index) c.RemoveNode(node) return -1 } //存在:从链表移除、添加到链表头部 c.RemoveNode(node) c.AddNode(node) return node.val} else { //不存在,返回-1 return -1}
}
```
**修改的点已经标记**。
## 2.4、Put修改
```csharp
func (c *LRUcache) Put(key int, val int, ttl time.Duration) {//修改1c.mu.Lock()defer c.mu.Unlock()//修改2,获取过期时间exp := time.Now().Add(ttl)//检查节点是否存在if node, ok := c.KeyToNode[key]; ok { //存在:更新节点的值、移除节点、添加节点到头部 //修改3,重新设置过期时间 node.expire = expnode.val = val c.RemoveNode(node) c.AddNode(node)//修改4,heap需要fix heap.Fix(&c.expHeap, node.index)} else { //不存在,先检查容量是否上限 if c.cnt == c.cap { c.RemoveTail() //移除尾部节点 } //容量足够,添加节点 newNode := &Node{key: key, val: val}//修改4,设置节点过期时间,添加到堆,唤醒协程 newNode.expire = exp c.AddNode(newNode) heap.Push(&c.expHeap, newNode) c.expCond.Signal()}
}
```
Put方法的流程:
- 获取锁
- 检查节点是否存在
- 存在:进行节点的移除、节点值更新、节点添加、heap修复
- 不存在:检查容量,容量超出则移除尾部节点;进行节点的创建,节点的添加,heap的添加,唤醒协程
于是,我们就完成了带过期时间的LRU。
## 2.5、代码全览
```csharp
package main
import ("container/heap""fmt""sync""time"
)
type Node struct {Next, Pre *Nodekey, val intindex intexpire time.Time
}
type DoubleLinkList struct {Head *NodeTail *Node
}
// 最小堆实现
type MinHeap []*Node
func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Swap(i, j int) {h[i], h[j] = h[j], h[i]h[i].index, h[j].index = i, j
}
func (h *MinHeap) Push(x interface{}) {n := h.Len()node := x.(*Node)node.index = n*h = append(*h, node)
}
func (h *MinHeap) Pop() interface{} {old := *hn := old.Len()node := old[n-1]node.index = -1*h = old[:n-1]return node
}
type LRUcache struct {doubleList DoubleLinkListKeyToNode map[int]*Nodecap, cnt int
expHeap MinHeapmu sync.MutexexpCond sync.Cond
}
func Constructor(cap int) *LRUcache {head := &Node{}tail := &Node{}head.Next = tailtail.Pre = headlru := &LRUcache{ doubleList: DoubleLinkList{ Head: head, Tail: tail, }, cap: cap, KeyToNode: make(map[int]*Node, cap),}heap.Init(&lru.expHeap) //初始化堆lru.expCond = *sync.NewCond(&lru.mu) //创建condgo lru.expirer()return lru
}
func (c *LRUcache) expirer() {for { //获取锁 c.mu.Lock() //若列表没有节点,就沉睡等到被put唤醒 for c.expHeap.Len() == 0 { c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。 } //获取堆顶节点 node := c.expHeap[0] now := time.Now() wait := node.expire.Sub(now) //如果wait>0,说明还没有过期 if wait > 0 { //沉睡到该节点过期 c.mu.Unlock() time.Sleep(wait) //醒来后,要重新执行一次流程 continue } //清理这个节点 heap.Pop(&c.expHeap) c.RemoveNode(node) c.mu.Unlock()}
}
func (c *LRUcache) AddNode(node *Node) {//哈希表注册这个节点c.KeyToNode[node.key] = node//添加到链表中,头节点之后node.Next = c.doubleList.Head.Nextnode.Pre = c.doubleList.Headc.doubleList.Head.Next.Pre = nodec.doubleList.Head.Next = node//更新表记录节点数c.cnt++
}
func (c *LRUcache) RemoveNode(node *Node) {//哈希表删除节点记录delete(c.KeyToNode, node.key)//更新链表node.Next.Pre = node.Prenode.Pre.Next = node.Next//更新计数器c.cnt--
}
func (c *LRUcache) RemoveTail() {//获取尾部节点node := c.doubleList.Tail.Pre//移除c.RemoveNode(node)
}
func (c *LRUcache) Get(key int) int {//修改1c.mu.Lock()defer c.mu.Unlock()//查询节点是否存在if node, ok := c.KeyToNode[key]; ok { //修改2,检查是否过期 if time.Now().After(node.expire) { //过期了,协程还没有发现,手动帮助删除 heap.Remove(&c.expHeap, node.index) c.RemoveNode(node) return -1 } //存在:从链表移除、添加到链表头部 c.RemoveNode(node) c.AddNode(node) return node.val} else { //不存在,返回-1 return -1}
}
func (c *LRUcache) Put(key int, val int, ttl time.Duration) {//修改1c.mu.Lock()defer c.mu.Unlock()//修改2,获取过期时间exp := time.Now().Add(ttl)//检查节点是否存在if node, ok := c.KeyToNode[key]; ok { //存在:更新节点的值、移除节点、添加节点到头部 //修改3,重新设置过期时间 node.expire = expnode.val = val c.RemoveNode(node) c.AddNode(node)//修改4,heap需要fix heap.Fix(&c.expHeap, node.index)} else { //不存在,先检查容量是否上限 if c.cnt == c.cap { c.RemoveTail() //移除尾部节点 } //容量足够,添加节点 newNode := &Node{key: key, val: val}//修改4,设置节点过期时间,添加到堆,唤醒协程 newNode.expire = exp c.AddNode(newNode) heap.Push(&c.expHeap, newNode) c.expCond.Signal()}
}
func (c *LRUcache) Print() {for node := c.doubleList.Head; node != nil; node = node.Next { fmt.Printf("%d -> ", node.key)}fmt.Println()
}
func main() {cache := Constructor(2)cache.Put(10, 101, time.Second)cache.Print()
time.Sleep(time.Second)fmt.Println(cache.Get(10))
cache.Put(9, 99, time.Second*2)cache.Print()
cache.Put(7, 77, time.Second)cache.Print()
time.Sleep(2 * time.Second)cache.Print()
}
```
# 3、测试
```csharp
func (c *LRUcache) Print() {for node := c.doubleList.Head; node != nil; node = node.Next { fmt.Printf("%d -> ", node.key)}fmt.Println()
}
func main() {cache := Constructor(2)cache.Put(10, 101, time.Second)cache.Print()
time.Sleep(time.Second)fmt.Println(cache.Get(10))
cache.Put(9, 99, time.Second*2)cache.Print()
cache.Put(7, 77, time.Second)cache.Print()
time.Sleep(2 * time.Second)cache.Print()
}
```
输出如下:
```csharp
0 -> 10 -> 0 ->
-1
0 -> 9 -> 0 ->
0 -> 7 -> 9 -> 0 ->
0 -> 0 ->
```
可以看到,过期的节点已经被成功自动删除了,同时原本的LRU功能保持不变。
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/2486
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利
- 相关联分享
- Golang实现带图解的简易版过期LRU缓存