一 什么是LRU缓存
LRU(英文:Least Recently Used),中文叫做最近最少使用缓存。也就是说,我们设计一个固定最大容量的缓存,当达到最大容量之后,我们再往缓存里放数据时,会先把最近最少使用的那个元素删除,再放入最新的元素。
为什么要设计“固定最大容量的缓存”?在生产环境,每一份内存都是非常宝贵的,如果不设置最大容量上限,元素无休止的增长,总缓存大小势必会达到系统阈值或者内存大小,容易抛出OutOfMemoryError。
二 为什么我们要自己实现LRU缓存
很多语言里面没有开箱即用的LRU缓存工具。包括Java、Python、Go等等。最像LRU缓存的是Java中的LinkedHashMap,它是由HashMap实现的,里面的元素通过链表串起来,但是无法设置最大容量,不断往里面放元素还是会导致它无限制增长。
三 要实现LRU缓存,我们的诉求是什么
从上面的说法,我们得出,我们的LRU缓存需要具备:
- 快速存取元素。
- 有固定的最大容量,不会无限制增长。
- 达到最大容量后,再往缓存里面放入新元素时,会先把最近最少使用的元素删除,再放入新元素。
四 怎么来实现
有了诉求只有,我们的目的很明确,接下来就是如何实现了。
- 第一点诉求,快速存取。我们使用map(哈希表、映射)就可以了。
- 第二点诉求,固定最大容量。我们给这个map增加capacity(整数)字段来表示最大容量,当map中元素个数达到capacity时,我们要继续往里面放入新元素,则先把最近最少使用的元素删除再放入新元素。
- 第三点诉求,删除最近最少使用的元素。怎么找到最近最少使用的元素删除?这个也很简单,参考Java中的LinkedHashMap的做法,把map中的元素使用双向链表串起来,把最近最少使用的元素放在队列尾部,最近最常使用的元素放在头部就可以了。
4.1 设计Entry
我们知道,元素要放入map里面,那我们元素必须具备key和value字段。元素要用双向链表串起来,那元素必须具备pre和next字段。我们的Entry设计如下:
type Entry struct {
Key string
Value interface{}
pre *Entry
next *Entry
}
4.2 设计缓存
Entry设计完成,我们开始设计缓存。我们自定义缓存类型,里面使用map来存放缓存的元素,capacity就是我们所说的最大容量。
type Cache struct {
cache map[string]*Entry
capacity int
head *Entry
tail *Entry
}
为什么缓存里面需要head和tail指针?这是因为,我们把元素都用双向链表串起来之后,放入新元素和获取元素时,我们要把最近最常使用的元素放在队列头部,最近最少使用的元素从队列尾部删除,所以需要头尾指针帮我们实现这些操作。
我们新增NewCache方法,让外部通过这个方法来创建LRU缓存,同时传入最大容量:
func NewCache(cap int) *Cache {
return &Cache{cache: make(map[string]*Entry), capacity: cap}
}
同时,我们还要加把锁,防止并发修改的时候出现问题。
var lock sync.Mutex
4.2.1 往缓存中放元素
我们往缓存中放元素,有两种情况需要分析:
- 元素在缓存中已经存在了,则把元素的Value更新为我们传入的Value,把元素当做最近最常使用的元素,提到队列头部。
- 元素在缓存中不存在,则把元素直接放到队列头部。
第一种情况,元素已经存在
第一种情况很好处理,先判断存不存在,存在的话提到队列头部:
if existVal, exist := cache.cache[key]; exist {
// 元素存在,下面把元素放到最前面去
if existVal == cache.head {
// 元素已经在最前面了,什么都不用做
return
}
// 元素不在最前面,那么当前元素的前一个元素一定不为nil
existVal.pre.next = existVal.next
if existVal == cache.tail {
cache.tail = existVal.pre
} else {
existVal.next.pre = existVal.pre
}
existVal.pre = nil
existVal.next = cache.head
cache.head.pre = existVal
cache.head = existVal
return
}
重点看下面那一大段,当元素不在队列最前面时,需要把当前元素剥离,放在队列最前面去。
剥离元素,即,需要把这样:
变成这样:
对应的是这一段代码:
// 元素不在最前面,那么当前元素的前一个元素一定不为nil
existVal.pre.next = existVal.next
if existVal == cache.tail {
cache.tail = existVal.pre
} else {
existVal.next.pre = existVal.pre
}
existVal.pre = nil
existVal.next = cache.head
这里需要注意判断当前元素是不是队列最后一个元素,如果是, 需要把tail指针往前移。剥离完成之后,把existVal的next设置为head,head.pre设置为existVal,再把head设置为最新的头部元素,即existVal就可以了。
第二种情况,元素不存在
元素不存在的情况很好处理,直接把元素放到队列头部,元素放入缓存就可以了。
但是这里又有两种情况,第一种,当放入新元素之后,缓存元素个数还没达到最大容量,直接放入就好了。也就是下面这种情况。
放入之后,把head设置为当前元素。
对应代码如下:
e := &Entry{Key: key, Value: val, next: cache.head}
if cache.head != nil {
// head不为空,把当前元素e设置为head的前一个元素
cache.head.pre = e
}
队列head有变化,head设置为当前元素e
cache.head = e
if cache.tail == nil {
// 如果tail指针为空,说明队列刚刚就是空的,现在只有e一个元素,tail指针设置为e就可以了
cache.tail = e
}
// 队列操作完毕,把元素e放入缓存中
cache.cache[key] = e
if len(cache.cache) <= cache.capacity {
// 元素e放入缓存中后,元素个数还没有达到最大容量,放入元素到此结束。
return nil
}
另一种情况,当放入新元素之后,缓存元素个数达到最大容量,需要把队列尾部的元素删掉。
把tail指针的元素剥离,tail指针前移即可。对应代码如下:
removedEntry := cache.tail
cache.tail = cache.tail.pre
removedEntry.pre = nil
cache.tail.next = nil
delete(cache.cache, removedEntry.Key)
return removedEntry.Value
4.2.2 从缓存中获取元素
获取元素比放入元素要简单,获取元素时,如果元素不存在,直接返回nil就可以。如果元素存在,把元素当做最近最常使用的元素,提到队列头部。
即,把当前元素剥离队列,再放入头部即可:
对应代码如下:
if existVal, exist := cache.cache[key]; exist {
// 把该元素提到队列头部
// 元素存在,下面把元素放到最前面去
if existVal == cache.head {
return
}
// 元素不是head,那么existVal.pre一定不为nil
existVal.pre.next = existVal.next
if existVal == cache.tail {
cache.tail = existVal.pre
} else {
existVal.next.pre = existVal.pre
}
existVal.pre = nil
existVal.next = cache.head
cache.head.pre = existVal
cache.head = existVal
return existVal.Value
}
return nil
五 测试一下
测试代码如下:
func TestLRU(t *testing.T) {
cache := NewCache(2)
cache.Put("1", "one") // 放入元素one,此时one在队列头部
fmt.Println(cache.Get("1")) // 此处输出“one”,此时one在队列头部
cache.Put("2", "two") // 放入元素two,此时two在队列头部
fmt.Println(cache.Get("1")) // 此处输出“one”,此时one在队列头部
cache.Put("3", "three") // 放入元素three,总元素个数为3,因此最近最少使用的元素“2”会被删除
fmt.Println(cache.Get("2")) // 此处输出nil
fmt.Println(cache.Get("3")) // 此处输出“three”
fmt.Println(cache.Get("3")) // 此处输出“three”
fmt.Println(cache.Get("1")) // 此处输出“one”,此时最近最少使用的元素为“3”
cache.Put("2", "two") // 放入元素three,总元素个数为3,因此最近最少使用的元素“3”会被删除
fmt.Println(cache.Get("3")) // 此处输出nil
fmt.Println(cache.Get("1")) // 此处输出one
}
运行一下看看输出:
=== RUN TestLRU
one
one
<nil>
three
three
one
<nil>
one
--- PASS: TestLRU (0.00s)
PASS
跟我们预期的一致。
源码在Github:https://github.com/ychenracing/GoApps/blob/master/src/LRUCache/LRU.go
喜欢的点个Star。
喜欢的可以关注我的WeiXin订阅号,随时随地学习: