Go语言实现LRU缓存 | 青训营笔记

115 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

一、什么是缓存

这里说的缓存是一种广义的概念,在计算机存储层次结构中,低一层的存储器都可以看做是高一层的缓存。比如Cache是内存的缓存,内存是硬盘的缓存,硬盘是网络的缓存等等。

缓存可以有效地解决存储器性能与容量的这对矛盾,但绝非看上去那么简单。如果缓存算法设计不当,非但不能提高访问速度,反而会使系统变得更慢。

从本质上来说,缓存之所以有效是因为程序和数据的局部性(locality)。程序会按固定的顺序执行,数据会存放在连续的内存空间并反复读写。这些特点使得我们可以缓存那些经常用到的数据,从而提高读写速度。

缓存的大小是固定的,它应该只保存最常被访问的那些数据。然而未来不可预知,我们只能从过去的访问序列做预测,于是就有了各种各样的缓存替换策略。本文介绍一种简单的缓存策略,称为最近最少使用(LRU,Least Recently Used)算法。

二、LRU的实现

我们以内存访问为例解释缓存的工作原理。假设缓存的大小固定,初始状态为空。每发生一次读内存操作,首先查找待读取的数据是否存在于缓存中,若是,则缓存命中,返回数据;若否,则缓存未命中,从内存中读取数据,并把该数据添加到缓存中。向缓存添加数据时,如果缓存已满,则需要删除访问时间最早的那条数据,这种更新缓存的方法就叫做LRU。

实现LRU时,我们需要关注它的读性能和写性能,理想的LRU应该可以在O(1)的时间内读取一条数据或更新一条数据,也就是说读写的时间复杂度都是O(1)。

此时很容易想到使用HashMap,根据数据的键访问数据可以达到O(1)的速度。但是更新缓存的速度却无法达到O(1),因为需要确定哪一条数据的访问时间最早,这需要遍历所有缓存才能找到。

因此,我们需要一种既按访问时间排序,又能在常数时间内随机访问的数据结构。

代码实现:

// LRUCache LRU 缓存 https://leetcode.cn/problems/lru-cache/
/**
要点总结:
1. LRUCache结构体字段包括size,capacity,cache(Map),head和tail
2. 内部节点结构体,包含pre和next,k和v
3. 封装三个内部方法,moveToHead、addToHead,removeNode,removeTail
*/
type LRUCache struct {
   size       int
   capacity   int
   cache      map[int]*node
   head, tail *node
}

// node 内部节点
type node struct {
   k, v      int
   pre, next *node
}

func initNode(key, value int) *node {
   return &node{
      k: key,
      v: value,
   }
}

func Constructor(capacity int) LRUCache {
   lruCache := LRUCache{
      size:     0,
      capacity: capacity,
      cache:    map[int]*node{},
      head:     initNode(0, 0),
      tail:     initNode(0, 0),
   }
   lruCache.head.next = lruCache.tail
   lruCache.tail.pre = lruCache.head
   return lruCache
}

func (this *LRUCache) Get(key int) int {
   if _, ok := this.cache[key]; !ok {
      return -1
   }
   node := this.cache[key]
   this.moveToHead(node)
   return node.v
}

func (this *LRUCache) Put(key int, value int) {
   if _, ok := this.cache[key]; !ok {
      node := initNode(key, value)
      this.cache[key] = node
      this.addToHead(node)
      this.size++
      if this.size > this.capacity {
         tail := this.removeTail()
         delete(this.cache, tail.k)
         this.size--
      }
   } else {
      node := this.cache[key]
      node.v = value
      this.moveToHead(node)
   }
}

func (this *LRUCache) addToHead(node *node) {
   node.pre = this.head
   node.next = this.head.next
   this.head.next.pre = node
   this.head.next = node
}

func (this *LRUCache) removeNode(node *node) {
   node.pre.next = node.next
   node.next.pre = node.pre
}

func (this *LRUCache) moveToHead(node *node) {
   this.removeNode(node)
   this.addToHead(node)
}

func (this *LRUCache) removeTail() *node {
   node := this.tail.pre
   this.removeNode(node)
   return node
}