1)FIFO(先進先出)算法
核心思想:如果一個數據最先進入緩存,那么就應該最先刪掉。類似于隊列的思想。
實現:創建一個隊列(雙向鏈表),新增記錄,獲取記錄,以及當緩存滿了,自動淘汰記錄。
目錄:
image.png
cache.go : 公共的結構體,公共的方法
Cache 接口:定義一些接口方法。
package cache
import (
"fmt"
"runtime"
)
type Cache interface {
Set(key string, value interface{})
Get(key string) interface{}
Del(key string)
DelOldest()
Len() int
}
type Value interface {
Len() int
}
// 計算對應類型的字節數
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 = 8 + len(v)
}
case bool, uint8, int8:
n = 1
case int16, uint16:
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 implenment cache,.value", value))
}
return n
}
fifo.go : FIFO算法的實現。
package fifo
import (
"cache"
"container/list"
)
type fifo struct {
maxBytes int
onEvicted func(key string, value interface{})
usedBytes int
ll *list.List
cache map[string]*list.Element
}
type entry struct {
key string
value interface{}
}
func (e *entry) Len() int {
return cache.CalcLen(e.value)
}
// New 創建一個新的 Cache,如果 maxBytes 是 0,表示沒有容量限制
func New(maxBytes int, onEvicted func(key string, value interface{})) cache.Cache {
return &fifo{
maxBytes: maxBytes,
onEvicted: onEvicted,
ll: list.New(),
cache: make(map[string]*list.Element),
}
}
// Set 往 Cache 尾部增加一個元素(如果已經存在,則放入尾部,并修改值)
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()
}
}
// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (f *fifo) Get(key string) interface{} {
if e, ok := f.cache[key]; ok {
// LRU 算法, 這里將最近訪問的數據向后面移動,防止被刪除。
// f.ll.MoveToBack(e)
return e.Value.(*entry).value
}
return nil
}
// Del 從 cache 中刪除 key 對應的記錄
func (f *fifo) Del(key string) {
if e, ok := f.cache[key]; ok {
f.removeElement(e)
}
}
// DelOldest 從 cache 中刪除最舊的記錄
func (f *fifo) DelOldest() {
f.removeElement(f.ll.Front())
}
// Len 返回當前 cache 中的記錄數
func (f *fifo) Len() int {
return f.ll.Len()
}
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)
}
}
2)LRU 算法 (最近最少使用)
核心思想: 如果數據被訪問過,那么將來訪問的概率會更高。這個思想和FIFO其實類似。只需要在每次訪問的數據將其移動到隊尾,這樣淘汰數據時,將不被容易被淘汰。
實現: 和FIFO基本一樣。在每次GET的時候,將訪問的數據移動到隊尾。
// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (f *fifo) Get(key string) interface{} {
if e, ok := f.cache[key]; ok {
// LRU 算法, 這里將最近訪問的數據向后面移動,防止被刪除。
// f.ll.MoveToBack(e)
return e.Value.(*entry).value
}
return nil
}
3)LFU 算法:最少使用。
核心思想:如果數據被訪問多次,那么以后被訪問的頻率更大。 其實就是維護一個權重,如果一個數據經常被訪問,權重越大,不容易被淘汰。
實現:使用堆。
lfu.go
package lfu
import (
"cache"
"container/heap"
)
type lfu struct {
maxBytes int
onEvicted func(key string, value interface{})
usedBytes int
queue *queue
cache map[string]*entry
}
// New 創建一個新的 Cache,如果 maxBytes 是 0,表示沒有容量限制
func New(maxBytes int, onEvicted func(key string, value interface{})) cache.Cache {
q := make(queue, 0, 1024)
return &lfu{
maxBytes: maxBytes,
onEvicted: onEvicted,
queue: &q,
cache: make(map[string]*entry),
}
}
// Set 往 Cache 增加一個元素(如果已經存在,更新值,并增加權重,重新構建堆)
func (l *lfu) Set(key string, value interface{}) {
if e, ok := l.cache[key]; ok {
l.usedBytes = l.usedBytes - cache.CalcLen(e.value) + cache.CalcLen(value)
l.queue.update(e, value, e.weight+1)
return
}
en := &entry{key: key, value: value}
heap.Push(l.queue, en)
l.cache[key] = en
l.usedBytes += en.Len()
if l.maxBytes > 0 && l.usedBytes > l.maxBytes {
l.removeElement(heap.Pop(l.queue))
}
}
// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (l *lfu) Get(key string) interface{} {
if e, ok := l.cache[key]; ok {
l.queue.update(e, e.value, e.weight+1)
return e.value
}
return nil
}
// Del 從 cache 中刪除 key 對應的元素
func (l *lfu) Del(key string) {
if e, ok := l.cache[key]; ok {
heap.Remove(l.queue, e.index)
l.removeElement(e)
}
}
// DelOldest 從 cache 中刪除最舊的記錄
func (l *lfu) DelOldest() {
if l.queue.Len() == 0 {
return
}
l.removeElement(heap.Pop(l.queue))
}
// Len 返回當前 cache 中的記錄數
func (l *lfu) Len() int {
return l.queue.Len()
}
func (l *lfu) removeElement(x interface{}) {
if x == nil {
return
}
en := x.(*entry)
delete(l.cache, en.key)
l.usedBytes -= en.Len()
if l.onEvicted != nil {
l.onEvicted(en.key, en.value)
}
}
queue.go : 通過heap來實現最小堆,需要實現heap.Interface接口。
package lfu
import (
"cache"
"container/heap"
)
type entry struct {
key string
value interface{}
weight int
index int
}
func (e *entry) Len() int {
return cache.CalcLen(e.value) + 4 + 4
}
type queue []*entry
func (q queue) Len() int {
return len(q)
}
func (q queue) Less(i, j int) bool {
return q[i].weight < q[j].weight
}
func (q queue) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].index = i
q[j].index = j
}
func (q *queue) Push(x interface{}) {
n := len(*q)
en := x.(*entry)
en.index = n
*q = append(*q, en)
}
func (q *queue) Pop() interface{} {
old := *q
n := len(old)
en := old[n-1]
old[n-1] = nil // avoid memory leak
en.index = -1 // for safety
*q = old[0 : n-1]
return en
}
// update modifies the weight and value of an entry in the queue.
func (q *queue) update(en *entry, value interface{}, weight int) {
en.value = value
en.weight = weight
heap.Fix(q, en.index)
}
總結:
1 FIFO算法缺點:有些數據被經常訪問,但是也會經常淘汰掉。
2 LRU算法:groupcache庫使用的LRU。
3 LFU算法:對剛加入的數據不友好,因為相比于歷史數據,其權重更低。同時更新數據,訪問數據都會去更新堆,影響性能。