这是我参与「第三届青训营 -后端场」笔记创作活动的的第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
}