golang中实现LRU-K算法(附带单元测试)

        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 lru

import "container/list"

// lru 缓存淘汰策略
// Cache is a LRU cache. It is not safe for concurrent access.
type Cache struct {
	maxBytes int64 //允许使用的最大内存
	useBytes int64 //当前已使用的内存
	ll       *list.List
	mp       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 int64
	useBytes int64
	ll       *list.List // 历史队列是以FIFO为淘汰策略
	mp       map[string]*list.Element
	cnt      map[string]int // 对每个节点访问次数的统计
}

type entry struct {
	key   string
	value 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 {
		// 缓存命中了就挪到前面,更新value
		ele := 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] = ele
			c.historyCache.useBytes += int64(len(key)) + int64(value.Len())

			// 判断历史队列内存是否用完,历史队列的淘汰策略为FIFO
			if 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] = ele
	c.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 lru

import (
	"reflect"
	"testing"
)

type String string

func (d String) Len() int {
	return len(d)
}

// 只针对于LRU的测试,即LRU-1

func 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算法

相关推荐

  1. golang实现LRU-K算法(附带单元测试)

    2024-07-20 10:00:05       20 阅读
  2. Golang 单元测试

    2024-07-20 10:00:05       49 阅读
  3. golang代码规范和单元测试

    2024-07-20 10:00:05       46 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 10:00:05       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 10:00:05       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 10:00:05       45 阅读
  4. Python语言-面向对象

    2024-07-20 10:00:05       55 阅读

热门阅读

  1. 23年阿里淘天笔试题 | 卡码网模拟

    2024-07-20 10:00:05       17 阅读
  2. 前端经验:使用sheetjs导出CSV文本为excel

    2024-07-20 10:00:05       16 阅读
  3. autohotkey自动化执行vim命令

    2024-07-20 10:00:05       20 阅读
  4. 开源虚拟加密盘VeraCrypt命令行使用方法

    2024-07-20 10:00:05       13 阅读
  5. DP 203 学习笔记

    2024-07-20 10:00:05       16 阅读
  6. python实现建立一个学生成绩管理系统

    2024-07-20 10:00:05       19 阅读
  7. redis是如何实现过期时间一到就删除key

    2024-07-20 10:00:05       20 阅读