lru.Cache
lru 源码位置 https://github.com/golang/groupcache/tree/master/lru
LRU(Least Recently Used)
- 特点:即最近最少使用,优先移除最久未使用的数据,按时间维度衡量。
- 数据结构:双向链表和HashMap
- 淘汰原则:根据缓存数据使用的时间,将最不经常使用的缓存数据优先淘汰。如果一个数据在最近一段时间内都没有被访问,那么在将来他被访问的可能性也很小
先看一下Cache代码结构
type Cache struct {
// MaxEntries is the maximum number of cache entries before
// an item is evicted. Zero means no limit.
MaxEntries int // 缓存容量限制
// OnEvicted optionally specifies a callback function to be
// executed when an entry is purged from the cache.
OnEvicted func(key Key, value interface{}) // 删除的时候可以指定一个回调函数
ll *list.List // 以根节点为起点的一个环形双向链表
cache map[interface{}]*list.Element // 以map形式
}
存储的值的结构
type Key interface{}
type entry struct {
key Key
value interface{}
}
初始化方法
func New(maxEntries int) *Cache {
return &Cache{
MaxEntries: maxEntries, // 指定容量
ll: list.New(),
cache: make(map[interface{}]*list.Element),
}
}
Cache.ll对象的初始化详解
// New returns an initialized list.
func New() *List { return new(List).Init() }
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
///
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
可以先看List这个结构,这是go SDK自带的链表结构(“container/list”),而lru.Cache就是建立在List之上的。List是一个双向环形链表,每一个节点叫做Element。len表示List的长度
Element
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element // 前后对象的指针
// The list to which this element belongs.
list *List // 所属的List
// The value stored with this element.
Value interface{} // 这个元素对应的值
}
接下来我们逐一分析一下lru给我们提供的方法,从方法中就可以看出来lru的工作思路
Add方法
// Add adds a value to the cache.
func (c *Cache) Add(key Key, value interface{}) {
if c.cache == nil { // 如果cache是nil,先进行初始化
c.cache = make(map[interface{}]*list.Element)
c.ll = list.New()
}
if ee, ok := c.cache[key]; ok { // 已经存在于缓存
c.ll.MoveToFront(ee) // 把ee移动到list根节点后面
ee.Value.(*entry).value = value
return
}
ele := c.ll.PushFront(&entry{key, value}) // 把value插入到root的后面
c.cache[key] = ele // 添加到map里面
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { // 链表长度超限
c.RemoveOldest()
}
}
这里面涉及到3个方法
- c.ll.MoveToFront(ee) --调用List的方法,基于元素已经存在于cache,需要更新
- c.ll.PushFront --调用List的方法,元素不在cache里面,需要add
- c.RemoveOldest() --自身封装的方法
- c.ll.MoveToFront(ee)
func (l *List) MoveToFront(e *Element) { // 元素移动到前面位置
if e.list != l || l.root.next == e { // 这个元素不属于此List,或者此元素刚好已经在根节点下的第一个元素,则不需要移动此元素
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root) // 把这个元素移动到root下的第一个元素
}
// --------------------------------
func (l *List) move(e, at *Element) *Element {
if e == at { // 自身就是根,直接返回
return e
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
return e
}
move里面的代码我用一个简单的示意图表示一下会比较容易看明白
可以看到move动作是把一个元素移动到了根元素的下一位。
2. c.ll.PushFront PushFront添加一个元素到cache的过程
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v interface{}) *Element {
l.lazyInit()
return l.insertValue(v, &l.root)
}
先看懒加载 lazyInit,如果root的下一个元素为nil,说明没有初始化,调用Init初始化,上面已经讲过初始化了
// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
下面是insertValue
func (l *List) insertValue(v interface{}, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l // e元素属于这个list(l)
l.len++ // 长度 +1
return e
}
插入动作其实很简单
新添加的元素紧随根的后面
3. c.RemoveOldest() 删除最后一个元素
// RemoveOldest removes the oldest item from the cache.
func (c *Cache) RemoveOldest() {
if c.cache == nil {
return
}
ele := c.ll.Back()
if ele != nil {
c.removeElement(ele)
}
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}
c.ll.Back() 返回根的前一个元素,从上图的环形双向链表可以看到,root的prev就是从根顺序遍历的最后一个元素
func (c *Cache) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*entry)
delete(c.cache, kv.key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
}
}
先跳过c.ll.Remove(e),删除map里面的值,如果有清理的函数,就执行一下清理函数,下面再看c.ll.Remove(e)
func (l *List) Remove(e *Element) interface{} {
if e.list == l { // 如果e属于list链表
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e) // 移除e
}
return e.Value
}
// remove removes e from its list, decrements l.len, and returns e.
func (l *List) remove(e *Element) *Element {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
return e
}
操作环形链表的倒数第二个元素,下一个指向根root,根的上一个指向倒数第二个;然后在把e的属性清空;长度 -1
到这Add方法就完了,总结一下:来一个元素,先判断是否存在于cache中,如果存在,则把元素移动到根节点的下一个位置,更新元素对应的值;如果不存在,则把元素加入到cache中,放到根节点的下一个位置。接下来判断cache中的元素有没有超过容量限制,如果超过了,则把链表的最后一个元素删除掉;如果没有超过,结束。
Get()方法
// Get looks up a key's value from the cache.
func (c *Cache) Get(key Key) (value interface{}, ok bool) {
if c.cache == nil { // cache为空,直接返回
return
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele) // 前面讲过了,把ele移动到根节点的下一个位置
return ele.Value.(*entry).value, true
}
return
}
Remove()方法
// Remove removes the provided key from the cache.
func (c *Cache) Remove(key Key) {
if c.cache == nil { // cache为空,直接返回
return
}
if ele, hit := c.cache[key]; hit { // 如果缓存中存在这个元素
c.removeElement(ele) // 移除掉这个元素,前面讲过了
}
}
RemoveOldest()方法
func (c *Cache) RemoveOldest() {
if c.cache == nil { // cache为空,直接返回
return
}
ele := c.ll.Back() // 获取最后一个元素
if ele != nil {
c.removeElement(ele) // 移除这个元素
}
}
Len()
func (c *Cache) Len() int {
if c.cache == nil {
return 0
}
return c.ll.Len() // 返回链表的长度
}
Clear()方法
// Clear purges all stored items from the cache.
func (c *Cache) Clear() {
if c.OnEvicted != nil {
for _, e := range c.cache {
kv := e.Value.(*entry)
c.OnEvicted(kv.key, kv.value)
}
}
c.ll = nil
c.cache = nil
}
这个前面没有讲到,但是也比较容易,如果没有定义清理的回调函数的话,直接把Cache的属性都置空就行,如果定义了回调函数,则先遍历所有的元素,执行回调函数调用,在清理Cache
总结:lru.Cache 不支持并发操作。cache提供元素hash存储,支持高效查询;ll是一个环形双向链表,来记录元素的操作顺序。从上面提供的方法可以看出,整个链表把所有的元素按照时间上操作顺序排序,最近操作的放在最前面,可以删除指定元素,也可以删除最后一个元素(最长时间为被使用)。更新,插入都使用Add方法完成。提供查询功能