FIFO缓存 | 青训营笔记

396 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记

1 FIFO淘汰算法

FIFO(First In First Out,先进先出)淘汰算法是淘汰缓存中最早的记录。FIFO Cache的设计中,核心原则就是:如果一个数据最先进入缓存,那么也应该最早淘汰掉。这么认为的根据是,最早添加的记录,其不再被使用的可能性比刚添加的可能性大。

这种算法的实现非常简单,创建一个队列(一般通过双向链表实现),新增记录添加到队尾,缓存满了,淘汰队首

2 FIFO算法实现

2.1 定义FIFO缓存结构体

type fifo struct {
    // 最大容量
    maxBytes int
    // 当一个 entry 从缓存中移除是调用该回调函数,默认为 nil
    onEvicted func(key string, value interface{})
    // 已经使用的字节数,只包括值,key不算
    usedBytes int

    ll    *list.List
    cache map[string]*list.Element
}

该结构体中,核心的两个数据结构是 *list.Listmap[string]*list.Elementlist.List 中存放真实内容,map 的键是字符串,值是指向键所对应内容在 list.List 中的位置的指针。

image.png

  • map 用来存储键值对。这是实现缓存最简单直接的数据结构。因为它的查找和增加时间复杂度都是 O(1)。
  • list.List 是 Go 标准库提供的双向链表。通过这个数据结构存放具体的值,可以做到移动记录到队尾的时间复杂度是 O(1),在在队尾增加记录时间复杂度也是 O(1),同时删除一条记录时间复杂度同样是 O(1)。

2.2 定义指针

type entry struct {
   key   string
   value interface{}
}

func (e *entry) Len() int {
   return cache.CalcLen(e.value)
}

2.3 定义内存计算函数

这里考虑内存控制,只计算value占用的内存。

func CalcLen(value interface{}) int {
   var n int
   switch v := value.(type) {
   case Value:
      n = v.Len()
   case string:
      if runtime.GOARCH == "amd64" {
         n = 16 + len(v)
      } else {
         n = n + len(v)
      }
   case bool, uint8, int8:
      n = 1
   case uint16, int16:
      n = 2
   case int32, uint32, float32:
      n = 4
   case int64, uint64, float64:
      n = 8
   case int, uint:
      if runtime.GOARCH == "amd64" {
         n = 8
      } else {
         n = 4
      }
   case complex64:
      n = 8
   case complex128:
      n = 16
   default:
      panic(fmt.Sprintf("%T is not implement cache.Value", value))
   }

   return n
}

cache.Value 接口定义

type Value interface {
	Len() int
}

2.4 定义缓存创建函数

func New(maxBytes int, onEvicted func(key string, value interface{})) *fifo {
   return &fifo{
      maxBytes:  maxBytes,
      onEvicted: onEvicted,
      ll:        list.New(),
      cache:     make(map[string]*list.Element),
   }
}

2.5 定义功能函数

func (f *fifo) Set(key string, value interface{}) {
   if e, ok := f.cache[key]; ok {
      f.ll.MoveToBack(e)
      en := e.Value.(*entry)
      f.usedBytes = f.usedBytes - cache.CalcLen(en.value) + cache.CalcLen(value)
      en.value = value
      return
   }

   en := &entry{key, value}
   e := f.ll.PushBack(en)
   f.cache[key] = e

   f.usedBytes += en.Len()
   if f.maxBytes > 0 && f.usedBytes > f.maxBytes {
      f.DelOldest()
   }
}

func (f *fifo) Get(key string) interface{} {
   if e, ok := f.cache[key]; ok {
      return e.Value.(*entry).value
   }

   return nil
}

func (f *fifo) Del(key string) {
   if e, ok := f.cache[key]; ok {
      f.removeElement(e)
   }
}

func (f *fifo) DelOldest() {
   f.removeElement(f.ll.Front())
}

func (f *fifo) removeElement(e *list.Element) {
   if e == nil {
      return
   }

   f.ll.Remove(e)
   en := e.Value.(*entry)
   f.usedBytes -= en.Len()
   delete(f.cache, en.key)

   if f.onEvicted != nil {
      f.onEvicted(en.key, en.value)
   }
}

func (f *fifo) Len() int {
   return f.ll.Len()
}