GEE组件篇 -- LRU

文章介绍了LRU缓存的原理和在GEE组件中的实现,包括代码分析和测试。LRU通过双向链表和映射表实现O(1)复杂度的增删查改操作,当内存不足时淘汰最近最少使用的数据。提供的测试用例验证了LRU的功能,如超出大小限制后自动删除最旧项和回调函数的执行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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则不做主动的数据回收操作。
    • llGo提供的双向链表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框架里面是怎么发挥作用的。🧑🏼‍💻
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值