GEE组件篇 – LRU
详细可查看https://github.com/OddSpilit/gee/blob/master/gee/geecache/lru/lru.go 源码
LRU Cache
(最近最少使用)已经是很多变成人员耳熟能详的内存管理方式了。在实现其功能上也会有大概的一些思路,如往队列里面新增数据节点,修改已存在的节点,查询以及删除节点这些都是O(1)
复杂度的效率。LRU
作为内存数据淘汰策略,使用常见是当内存不足时,需要淘汰最近最少使用的数据。LRU常用语缓存系统的淘汰策略。- 在
GEE
里面LRU
也扮演者数据缓存管理的角色,我们分别从代码分析跟代码测试来解析这块的内容。👻👻
代码分析
- 两个数据结构,一个接口
-
Value
接口:定义一个Len()
方法,Cache
通过实现Len()
方法获取队列长度。 -
Cache
结构体:maxBytes
: 最大能存储的空间大小。nbytes
:目前已使用的空间大小,若maxBytes
设置为0则不做主动的数据回收操作。ll
:Go
提供的双向链表list.List
cache
:做队列元素映射表,可以通过O(1)
操作查询队列元素。OnEvited
:删除元素后的回调操作函数。
-
entry
结构体:存储在缓存列表的数据格式。以键值对的形式存在。 -
方法分析:
-
New
:返回实例化对象func New(maxBytes int64, onEvicted func(string, Value)) *Cache { return &Cache{ maxBytes: maxBytes, nbytes: 0, ll: list.New(), cache: make(map[string]*list.Element), OnEvicted: onEvicted, } }
-
Get
:查找列表对象func (c *Cache) Get(key string) (value Value, ok bool) { if ele, ok := c.cache[key]; ok { // 查找到之后会将对象放置在最前面以免被最快淘汰 c.ll.MoveToFront(ele) kv := ele.Value.(*entry) return kv.value, true } return }
-
RemoveOldest
:移除不被经常访问的对象节点func (c *Cache) RemoveOldest() { ele := c.ll.Back() if ele != nil { c.ll.Remove(ele) kv := ele.Value.(*entry) delete(c.cache, kv.key) // 同时删除映射表对应节点 c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) // 如果有回调操作则会执行 if c.OnEvicted != nil { c.OnEvicted(kv.key, kv.value) } } }
-
Add
:新增节点,若节点已存在,则将该节点挪到最前面func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { // "modify" 操作,移动节点位置 c.ll.MoveToFront(ele) kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) // 计算当前nbytes大小 kv.value = value } else { // 新增操作,直接放置节点到最前面 ele := c.ll.PushFront(&entry{key, value}) c.cache[key] = ele c.nbytes += int64(len(key)) + int64(value.Len()) // 计算当前nbytes大小 } // 超出大小限制, for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } }
-
Len
:获取LRU
当前长度:func (c *Cache) Len() int { return c.ll.Len() }
-
代码测试
package lru
import (
"reflect"
"testing"
)
type String string
func (s String) Len() int {
return len(s)
}
// 测试获取操作
func TestGet(t *testing.T) {
lru := New(int64(0), nil)
lru.Add("key1", String("1234"))
if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" {
t.Fatalf("cache hit key1=1234 failed")
}
if _, ok := lru.Get("key2"); ok {
t.Fatalf("cache miss key2 failed")
}
}
// 测试超出大小限制之后key1是否会被删除
func TestRemoveOldest(t *testing.T) {
k1, k2, k3 := "key1", "key2", "k3"
v1, v2, v3 := "value1", "value2", "v3"
cap := len(k1 + k2 + v1 + v2)
lru := New(int64(cap), nil)
lru.Add(k1, String(v1))
lru.Add(k2, String(v2))
lru.Add(k3, String(v3))
if _, ok := lru.Get("key1"); ok || lru.Len() != 2 {
t.Fatalf("Removeoldest key1 failed")
}
}
// 测试回调操作是否会被执行
func TestOnEvicted(t *testing.T) {
keys := make([]string, 0)
callback := func(key string, value Value) {
keys = append(keys, key)
}
lru := New(int64(10), callback)
lru.Add("key1", String("123456"))
lru.Add("k2", String("k2"))
lru.Add("k3", String("k3"))
lru.Add("k4", String("k4"))
expect := []string{"key1", "k2"}
if !reflect.DeepEqual(expect, keys) {
t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect)
}
}
- 这一篇章的代码逻辑都比较简单,很适合大家自己动手实践实现一下这块逻辑,无非其实就是三块内容,
缓存主体
,缓存数据格式
,以及缓存主体相对应的函数处理
。
总结
LRU
虽然简单,但确实在很多开发场景我们都会用到该内存管理设计。- 下一篇章我们会来讲一下
一致性hash
,讲解一下他的一些使用场景以及其在gee
框架里面是怎么发挥作用的。🧑🏼💻