LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。
LRU-K 算法代码:
lru-k.go
package lruimport "container/list"// lru 缓存淘汰策略
// Cache is a LRU cache. It is not safe for concurrent access.
type Cache struct {maxBytes int64 //允许使用的最大内存useBytes int64 //当前已使用的内存ll *list.Listmp map[string]*list.Element //键是字符串,值是双向链表中对应节点的指针// optional and executed when an entry is purged.OnEvicted func(key string, value Value)historyCache HistoryCache // 历史队列,只有访问次数达到k次后才会加入到缓存中
}type HistoryCache struct {k int // k次访问后加入缓存maxBytes int64useBytes int64ll *list.List // 历史队列是以FIFO为淘汰策略mp map[string]*list.Elementcnt map[string]int // 对每个节点访问次数的统计
}type entry struct {key stringvalue Value
}// Value use Len to count how many bytes it takes
type Value interface {Len() int
}// New is the Constructor of Cache
func New(maxBytes int64, onEvicted func(string, Value), k int) *Cache {return &Cache{maxBytes: maxBytes,ll: list.New(),mp: make(map[string]*list.Element),OnEvicted: onEvicted,//将某个函数传递给 New 函数,并赋给 OnEvicted 字段,你可以在缓存中的条目被移除时执行自定义的操作,//比如释放资源、记录日志等,可以让 Cache 结构体更加通用和可扩展。historyCache: HistoryCache{k: k, // 可以改为New()传入参,一般用2次命中率和适应性综合考虑最优maxBytes: maxBytes,ll: list.New(),mp: make(map[string]*list.Element),cnt: make(map[string]int),},}
}// Get look ups a key's value
func (c *Cache) Get(key string) (value Value, ok bool) {if _, ok = c.mp[key]; ok {// 缓存命中了就挪到前面ele := c.mp[key]c.ll.MoveToFront(ele)kv := ele.Value.(*entry)return kv.value, true} else {// 缓存未命中,去历史队列查看,如果访问次数达到k次需要加入到缓存中if _, ok = c.historyCache.mp[key]; ok {// 有就根据访问次数看是否要加到缓存中,没达到次数也要将该节点挪到最后,即最晚被FIFO淘汰c.historyCache.cnt[key]++ele := c.historyCache.mp[key]kv := ele.Value.(*entry)if c.historyCache.cnt[key] >= c.historyCache.k {c.AddToCache(key, value)// 加入缓存后,将该节点从历史队列中删除c.historyCache.ll.Remove(ele)c.historyCache.useBytes -= int64(kv.value.Len()) + int64(len(kv.key))delete(c.historyCache.mp, kv.key)delete(c.historyCache.cnt, kv.key)} else {c.historyCache.ll.MoveToBack(ele)}return kv.value, true} else {// 历史队列也没有就直接返回return}}return
}// Add adds a value to the cache.
func (c *Cache) Add(key string, value Value) {if _, ok := c.mp[key]; ok {// 缓存命中了就挪到前面,更新valueele := c.mp[key]c.ll.MoveToFront(ele)kv := ele.Value.(*entry)c.useBytes += int64(value.Len()) - int64(kv.value.Len())kv.value = value} else {// 缓存未命中,则去历史队列查看是否存在if _, ok = c.historyCache.mp[key]; !ok {// 没有就新增ele := c.historyCache.ll.PushBack(&entry{key, value})c.historyCache.cnt[key]++c.historyCache.mp[key] = elec.historyCache.useBytes += int64(len(key)) + int64(value.Len())// 判断历史队列内存是否用完,历史队列的淘汰策略为FIFOif c.historyCache.maxBytes != 0 && c.historyCache.maxBytes < c.historyCache.useBytes {c.RemoveHistoryCacheOldest()}} else {// 有就更新value,并移到队尾c.historyCache.cnt[key]++ele := c.historyCache.mp[key]c.historyCache.ll.MoveToBack(ele)kv := ele.Value.(*entry)c.historyCache.useBytes += int64(value.Len()) - int64(kv.value.Len())kv.value = value}// 判断是否达到加入缓存标准if c.historyCache.cnt[key] >= c.historyCache.k {c.AddToCache(key, value)ele := c.historyCache.mp[key]kv := ele.Value.(*entry)// 加入缓存后,将该节点从历史队列中删除c.historyCache.ll.Remove(ele)c.historyCache.useBytes -= int64(kv.value.Len()) + int64(len(kv.key))delete(c.historyCache.mp, kv.key)delete(c.historyCache.cnt, kv.key)}}
}func (c *Cache) AddToCache(key string, value Value) {ele := c.ll.PushFront(&entry{key, value})c.mp[key] = elec.useBytes += int64(len(key)) + int64(value.Len())//保证内存不超过最大值 ps:maxBytes为0表示无限制for c.maxBytes != 0 && c.maxBytes < c.useBytes {c.RemoveCacheOldest()}
}// RemoveCacheOldest removes the oldest item
func (c *Cache) RemoveCacheOldest() {ele := c.ll.Back()if ele != nil {c.ll.Remove(ele)kv := ele.Value.(*entry)delete(c.mp, kv.key)c.useBytes -= int64(kv.value.Len()) + int64(len(kv.key))if c.OnEvicted != nil {c.OnEvicted(kv.key, kv.value)}}
}func (c *Cache) RemoveHistoryCacheOldest() {ele := c.historyCache.ll.Front()if ele != nil {c.historyCache.ll.Remove(ele)kv := ele.Value.(*entry)delete(c.historyCache.mp, kv.key)delete(c.historyCache.cnt, kv.key)c.historyCache.useBytes -= int64(kv.value.Len()) + int64(len(kv.key))if c.OnEvicted != nil {c.OnEvicted(kv.key, kv.value)}}
}// Len is the number of cache entries
func (c *Cache) Len() int {return c.ll.Len()
}
单元测试代码:
lru-k_test.go
package lruimport ("reflect""testing"
)type String stringfunc (d String) Len() int {return len(d)
}// 只针对于LRU的测试,即LRU-1func TestGet(t *testing.T) {lru := New(int64(0), nil, 1) // 0表示无限制lru.Add("key1", String("123"))if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "123" {t.Fatalf("cache hit key1=123 failed")}if _, ok := lru.Get("key2"); ok {t.Fatalf("cache miss key2 failed")}
}func TestRemoveOldest(t *testing.T) {k1, k2, k3 := "key1", "key2", "key3"v1, v2, v3 := "value1", "value2", "value3"cap := len(k1 + v1 + k2 + v2)lru := New(int64(cap), nil, 1)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, 1)lru.Add("key1", String("123456"))lru.Add("k2", String("k2"))lru.Add("k3", String("k3"))lru.Add("k4", String("k4"))except := []string{"key1", "k2"}if !reflect.DeepEqual(except, keys) {t.Fatalf("Call OnEvicted failed, expect keys equals to %s", except)}}
参考:动手写分布式缓存 - GeeCache第一天 LRU 缓存淘汰策略 | 极客兔兔 (geektutu.com)
将LRU算法升级为了LRU-K算法