二、LRU缓存淘汰策略
package lru
import "container/list"
//lru 缓存,并发访问不安全
type Cache struct {
maxBytes int64 // 缓存的最大允许使用字节数
nbytes int64 // 当前缓存占用的字节数
ll *list.List // 维护缓存中元素的顺序
cache map[string]*list.Element // 哈希表旨在O(1)时间内找到key对应的value
OnEvicted func(key string, value Value) //可选,当一个键值对被移除时调用
}
type entry struct {
key string
value Value
}
type Value interface {
Len() int
}
// New is the Constructor of Cache
func New(maxBytes int64, onEvicted func(string, Value)) *Cache {
return &Cache{
maxBytes: maxBytes,
ll: list.New(),
cache: make(map[string]*list.Element),
OnEvicted: onEvicted,
}
}
// Add adds a value to the cache.
func (c *Cache) Add(key string, value Value) {
if ele, ok := c.cache[key]; ok { // 检查缓存,如果key已经存在,就将其移至队首并更新其value
c.ll.MoveToFront(ele)
kv := ele.Value.(*entry) // 类型断言,将ele转化为entry
c.nbytes += int64(value.Len()) - int64(kv.value.Len())
kv.value = value
} else {
ele := c.ll.PushFront(&entry{key, value})
c.cache[key] = ele
c.nbytes += int64(len(key)) + int64(value.Len())
}
for c.maxBytes != 0 && c.maxBytes < c.nbytes {
c.RemoveOldest()
}
}
// Get look up a key's value
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) // 将元素转化为entry
return kv.value, true
}
return
}
// RemoveOldest removes the oldest item
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)
}
}
}
// Len the number of cache entries
func (c *Cache) Len() int {
return c.ll.Len()
}
思考:
-
既然Get元素的时候,使用的是map进行查询,为什么要用一个双向链表来维护缓存中的元素的顺序?
因为缓存的大小是有限的,维护元素的顺序可以使得经常被访问的元素置于队首,这样当缓存满了,就可以直接淘汰队尾的元素,以腾出空间给新的元素。这也是LRU算法的思想。
-
在Add方法中,既然设置了缓存的最大容量,那为什么还允许当前已使用的容量可以超过最大值,为什么不在Add的时候去检查,当前要存储的value是否有足够的空间分配,如果有就存储,如果没有,在去进行删除最旧的缓存项?
- 性能考虑(减少不必要的检查)
- 在每次添加新数据时都进行空间检查,可能会引入额外的性能开销。尤其是在需要频繁更新缓存的场景下,检查是否有足够的空间可能会带来不必要的开销。相比之下,先添加数据,然后通过一个单独的逻辑来清理缓存,能够将检查和清理分开,减少每次添加数据时的复杂度。
- 批量删除缓存项的效率
- 有些缓存实现会采用延迟删除策略,即不在每次添加时都做删除操作,而是在超出容量限制时统一进行删除。这样做可以提高删除操作的效率,因为每次删除操作会涉及到需要淘汰多少旧项的问题。如果每次添加时都进行删除,可能会导致频繁且不必要的删除操作,从而影响性能。
- 内存管理的灵活性
- 在一些实现中,缓存的大小可能会动态增长并压缩。即使你设置了
maxBytes
,缓存的大小可能会因为多次添加、删除等操作而波动。选择在缓存已满之后进行处理,可以保证缓存有足够的空间来容纳新的数据,并避免在每次添加时都进行检查。
- 在一些实现中,缓存的大小可能会动态增长并压缩。即使你设置了
- 实现简化
- 有些实现中可能希望保持代码的简洁性,避免在每次添加数据时都需要考虑空间是否足够的问题。通过在容量超限时统一删除最旧项,这样实现起来可能更加直观且易于维护。
- 性能考虑(减少不必要的检查)
本文作者:Drunker•L
本文链接:https://www.cnblogs.com/drunkerl/articles/18559320
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步