FIFO 缓存算法很简单,但也可以聊挺久

news/2024/11/29 0:22:20/文章来源:https://www.cnblogs.com/Damaer/p/18575497

作者:秦怀

1 简介

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,这句话就是整个计算机软件以及系统设计中的核心思想,而缓存对这一思想的一种实践。

缓存,总归会受到存储空间的限制,当缓存的空间不足的时候,如果在保持一定系统文档的情况下,还能兼顾到缓存命中率呢?这就需要我们选择合适的缓存淘汰算法。

缓存淘汰算法种类比较多,我们本次主要介绍 FIFO:

先进先出,类似队列的特性,淘汰缓存中最早添加的记录,该算法基于"最早被添加的数据,不再被使用的可能性最大"。要想实现这种算法,只需要使用一个队列维护数据,并同时维护一个数据最大容量,每次新增数据都添加到队尾,如果内存不够,那么就先淘汰掉队头的元素。

缺点:
FIFO 算法的缺点也很明显,有些数据虽然被添加的比较早,但是使用频率也会很高,在这种策略下会被频繁添加进去,过一会又被淘汰掉,缓存命中率不高。

2 核心原理

下面是一些设计的思路:

  • 存储元素使用队列,并且队列容量有一定限制。但是为了兼顾查询效率,需要同时使用哈希表来存储 key 和 Element 的映射。
  • 为了删除的时候,能找到前后元素,队列需要维护成 双向队列

基本操作如下,为了处理并发时程序错误,需要在修改数据的时候加锁:

  • Set :先查找是否存在,分别执行两种操作。
    • 新元素,追加在队尾。
    • 已存在的元素,直接修改。
  • Get:
    • 按照 Key 在 Hash 表中查询出来。
  • Delete:
    • 先查找到元素,然后从链表以及哈希表中删除,更新缓存状态。
  • Stat:获取存储的状态

3 实现

3.1 使用自带的双向链表结构

本小节是最简单粗暴的实现,直接使用原生的双向队列,下面是一些设计的思路,仅供参考。

3.1.1 实体设计

3.1.2 缓存状态

缓存总的状态维护,定义一个 stat.go 类:

type Stat struct {  // key 的个数  keyCount int64  // value 使用得最大的容量  maxBytes int64  // value 已使用得最大的容量  usedBytes int64  
}

3.1.3 核心接口

定义基础能力接口 cache.go,除了增删改查,还有获取缓存状态:

  
type Cache interface {  Set(key string, value []byte)  Get(key string) []byte  Del(key string)  Stat() Stat  
}

3.1.4 元素封装

每个元素中,保存着 key 以及值,值统一使用 byte 数组进行存储,之所以用 []byte ,不用具体的数据结构,是因为需要抽象,适配所有的类型。

但是使用 []byte 我们取值后还需要转成 struct{},比较消耗 CPU,为什么不使用 interface{} 呢?
网上看到的一个答案是:内存中存储过多的 interface{} 会造成较大的 GC 影响:Getting to Go: The Journey of Go's Garbage Collector - The Go Programming Language

type entry struct {  key   string  value []byte  
}

3.1.5 整体实现

fifo.go 文件中,存储元素使用队列,并且队列容量有一定限制。但是为了兼顾查询效率,需要同时使用哈希表来存储 key 和 Element 的映射。

type fifo struct {  // 为了快速找到该value  cache map[string]*list.Element  // 按照添加顺序保存的list  head *list.List  // 缓存数据  stat Stat  
}  

对外的初始化方法:

func New(maxBytes int64) Cache {  return &fifo{  cache: map[string]*list.Element{},  head:  list.New(),  stat: Stat{  keyCount:  0,  maxBytes:  maxBytes,  usedBytes: 0,  },   }}

下面就是具体的实现:

import (  "container/list"  
)  type fifo struct {  // 为了快速找到该value  cache map[string]*list.Element  // 按照添加顺序保存的list  head *list.List  // 缓存数据  stat Stat  
}  func (f *fifo) Set(key string, value []byte) {  if e, ok := f.cache[key]; ok && e != nil {  f.setDataExisted(e, value)  } else {  f.setDataNotExisted(key, value)  }}  func (f *fifo) setDataNotExisted(key string, value []byte) {  newEntry := &entry{  key:   key,  value: value,  }   if f.stat.maxBytes > 0 {  for f.stat.usedBytes+int64(len(key)+len(value)) > f.stat.maxBytes {  f.removeOldestElement()  }   }   e := f.head.PushBack(newEntry)  f.cache[key] = e  f.stat.usedBytes = f.stat.usedBytes + int64(len(key)+len(value))  f.stat.keyCount++  }  func (f *fifo) setDataExisted(e *list.Element, value []byte) {  originEntry := e.Value.(*entry)  originSize := len(originEntry.value)  beRemoved := false  if f.stat.maxBytes > 0 {  for (int64)(len(value))-(int64)(originSize) > f.stat.maxBytes-f.stat.usedBytes {  if f.head.Front() == e {  beRemoved = true  }  f.removeOldestElement()  }   }   if beRemoved {  f.setDataNotExisted(originEntry.key, value)  return  }  originEntry.value = value  f.stat.usedBytes = f.stat.usedBytes + (int64)(len(value)) - (int64)(originSize)  }  func (f *fifo) removeOldestElement() {  f.removeElement(f.head.Front())  
}  func (f *fifo) removeElement(e *list.Element) {  if e == nil {  return  }  // 双向链表的删除  f.head.Remove(e)  originEntry := e.Value.(*entry)  f.stat.keyCount--  // 重新计算使用空间  f.stat.usedBytes = f.stat.usedBytes - int64(len(originEntry.key)+len(originEntry.value))  // Hash表删除  delete(f.cache, originEntry.key)  
}  func (f *fifo) Get(key string) []byte {  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) Stat() Stat {  return f.stat  
}  func New(maxBytes int64) Cache {  return &fifo{  cache: map[string]*list.Element{},  head:  list.New(),  stat: Stat{  keyCount:  0,  maxBytes:  maxBytes,  usedBytes: 0,  },   }}

测试一下,编写测试类 cache_test.go

import (  "Go-Cache/00_fifo/character01"  "fmt"   . "github.com/smartystreets/goconvey/convey"  "testing")  func TestCache(t *testing.T) {  Convey("TestApplyFunc", t, func() {  cache := character01.New(100)  stat := fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{0 100 0}")  cache.Set("hello", []byte("world"))  cache.Set("hello2", []byte("world2"))  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 22}")  cache.Set("hello2", []byte("changeWorld2"))  value := cache.Get("hello2")  So(string(value), ShouldEqual, "changeWorld2")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 28}")  cache.Set("k1", []byte("longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongV1"))  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 94}")  cache.Set("hello2", []byte("newHelloWorld2newHelloWorld2"))  value = cache.Get("hello2")  So(string(value), ShouldEqual, "newHelloWorld2newHelloWorld2")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{1 100 34}")  cache.Set("num", character01.IntToBytes(1))  num := cache.Get("num")  So(character01.BytesToInt(num), ShouldEqual, 1)  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 45}")  cache.Set("num", nil)  So(cache.Get("num"), ShouldEqual, nil)  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 37}")  cache.Del("num")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{1 100 34}")  })}

3.2 增加锁

但是明显上面的代码是有问题的,当并发到一定程度,就会出现并发写的 panic:

func TestNoLock(t *testing.T) {  // 包名是上面小节的包名cache := no_lock.New(100000)  num := 1000  var wg sync.WaitGroup  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Set("hello"+string(rune(index)), []byte("world"+string(rune(index))))  wg.Done() //5  }()  }   wg.Wait()  fmt.Printf("stat:%v\n", cache.Stat())  
}

panic 的报错:

fatal error: concurrent map read and map writegoroutine 40 [running]:
runtime.throw({0x1125cae?, 0x1?})/usr/local/go/src/runtime/panic.go:992 +0x71 fp=0xc000133698 sp=0xc000133668 pc=0x1033791
runtime.mapaccess2_faststr(0x2?, 0x0?, {0xc0000960a8, 0x6})/usr/local/go/src/runtime/map_faststr.go:117 +0x3d4 fp=0xc000133700 sp=0xc000133698 pc=0x1011c74
Go-Cache/00_fifo/character01.(*fifo).Set(0xc00006e4b0, {0xc0000960a8, 0x6}, {0xc0000960a0, 0x6, 0x8})

那为了实现并发安全,我们需要增加一把锁:

type fifo struct {  // 为了快速找到该value  cache map[string]*list.Element  // 按照添加顺序保存的list  head *list.List  // 缓存数据  stat  Stat  mutex sync.Mutex  
}

在所有可能修改数据的操作之前,加上锁, 比如 set :

func (f *fifo) Set(key string, value []byte) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok && e != nil {  f.setDataExisted(e, value)  } else {  f.setDataNotExisted(key, value)  }}

fifo.go 文件改造如下:

  
import (  "container/list"  "sync"   "time")  type fifo struct {  // 为了快速找到该value  cache map[string]*list.Element  // 按照添加顺序保存的list  head *list.List  // 缓存数据  stat  Stat  mutex sync.Mutex  
}  func (f *fifo) Set(key string, value []byte) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok && e != nil {  f.setDataExisted(e, value)  } else {  f.setDataNotExisted(key, value)  }}  func (f *fifo) setDataNotExisted(key string, value []byte) {  newEntry := &entry{  key:   key,  value: value,  }   if f.stat.maxBytes > 0 {  for f.stat.usedBytes+int64(len(key)+len(value)) > f.stat.maxBytes {  f.removeOldestElement()  }   }   e := f.head.PushBack(newEntry)  f.cache[key] = e  f.stat.usedBytes = f.stat.usedBytes + int64(len(key)+len(value))  f.stat.keyCount++  }  func (f *fifo) setDataExisted(e *list.Element, value []byte) {  originEntry := e.Value.(*entry)  originSize := len(originEntry.value)  beRemoved := false  if f.stat.maxBytes > 0 {  for (int64)(len(value))-(int64)(originSize) > f.stat.maxBytes-f.stat.usedBytes {  if f.head.Front() == e {  beRemoved = true  }  f.removeOldestElement()  }   }   if beRemoved {  f.setDataNotExisted(originEntry.key, value)  return  }  originEntry.value = value  f.stat.usedBytes = f.stat.usedBytes + (int64)(len(value)) - (int64)(originSize)  }  func (f *fifo) removeOldestElement() {  f.removeElement(f.head.Front())  
}  func (f *fifo) removeElement(e *list.Element) {  if e == nil {  return  }  // 双向链表的删除  f.head.Remove(e)  originEntry := e.Value.(*entry)  f.stat.keyCount--  // 重新计算使用空间  f.stat.usedBytes = f.stat.usedBytes - int64(len(originEntry.key)+len(originEntry.value))  // Hash表删除  delete(f.cache, originEntry.key)  
}  func (f *fifo) Get(key string) []byte {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok {  return e.Value.(*entry).value  }  return nil  
}  
func (f *fifo) Del(key string) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok {  f.removeElement(e)  }}  
func (f *fifo) Stat() Stat {  return f.stat  
}  func New(maxBytes int64) Cache {  return &fifo{  cache: map[string]*list.Element{},  head:  list.New(),  stat: Stat{  keyCount:  0,  maxBytes:  maxBytes,  usedBytes: 0,  },   }}

再测试一下, 一切正常:

func TestCache(t *testing.T) {  cache := character02.New(100000)  num := 100  var wg sync.WaitGroup  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Set("hello"+string(rune(index)), []byte("world"+string(rune(index))))  wg.Done() //5  }()  }   wg.Wait()  fmt.Printf("stat:%v\n", cache.Stat())  
}

image.png

3.3 读写锁

上面的锁,是全局锁,也就是不管是读还是写,都会上锁,如果是读多写少的情况,我们希望读是可以并发的,只有写是独占的,这个时候就需要考虑读写锁了。(缓存不一定适合使用读写锁,这只是个思路,抛砖引玉。

  
import (  "container/list"  "sync"   "time")  type fifo struct {  // 为了快速找到该value  cache map[string]*list.Element  // 按照添加顺序保存的list  head *list.List  // 缓存数据  stat  Stat  mutex sync.RWMutex  
}  func (f *fifo) Set(key string, value []byte) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok && e != nil {  f.setDataExisted(e, value)  } else {  f.setDataNotExisted(key, value)  }}  func (f *fifo) setDataNotExisted(key string, value []byte) {  newEntry := &entry{  key:   key,  value: value,  }   if f.stat.maxBytes > 0 {  for f.stat.usedBytes+int64(len(key)+len(value)) > f.stat.maxBytes {  f.removeOldestElement()  }   }   e := f.head.PushBack(newEntry)  f.cache[key] = e  f.stat.usedBytes = f.stat.usedBytes + int64(len(key)+len(value))  f.stat.keyCount++  }  func (f *fifo) setDataExisted(e *list.Element, value []byte) {  originEntry := e.Value.(*entry)  originSize := len(originEntry.value)  beRemoved := false  if f.stat.maxBytes > 0 {  for (int64)(len(value))-(int64)(originSize) > f.stat.maxBytes-f.stat.usedBytes {  if f.head.Front() == e {  beRemoved = true  }  f.removeOldestElement()  }   }   if beRemoved {  f.setDataNotExisted(originEntry.key, value)  return  }  originEntry.value = value  f.stat.usedBytes = f.stat.usedBytes + (int64)(len(value)) - (int64)(originSize)  }  func (f *fifo) removeOldestElement() {  f.removeElement(f.head.Front())  
}  func (f *fifo) removeElement(e *list.Element) {  if e == nil {  return  }  // 双向链表的删除  f.head.Remove(e)  originEntry := e.Value.(*entry)  f.stat.keyCount--  // 重新计算使用空间  f.stat.usedBytes = f.stat.usedBytes - int64(len(originEntry.key)+len(originEntry.value))  // Hash表删除  delete(f.cache, originEntry.key)  
}  func (f *fifo) Get(key string) []byte {  f.mutex.RLock()  defer f.mutex.RUnlock()  // time.Sleep(1000000)  if e, ok := f.cache[key]; ok {  return e.Value.(*entry).value  }  return nil  
}  
func (f *fifo) Del(key string) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok {  f.removeElement(e)  }}  
func (f *fifo) Stat() Stat {  return f.stat  
}  func New(maxBytes int64) Cache {  return &fifo{  cache: map[string]*list.Element{},  head:  list.New(),  stat: Stat{  keyCount:  0,  maxBytes:  maxBytes,  usedBytes: 0,  },   }}

假设读取 Cache 是比较耗时的,我们加点耗时在 Get 操作中:

func (f *fifo) Get(key string) []byte {  f.mutex.RLock()  defer f.mutex.RUnlock()  time.Sleep(1000000)  if e, ok := f.cache[key]; ok {  return e.Value.(*entry).value  }  return nil  
}

测试的数据如下,值得注意的是:如果读竞争以及耗时不高,读写锁会适得其反。

  • 读写锁:cost time: 4.560145ms
  • 独占锁:cost time:12.012624518s
// cost time:4.560145ms  
func TestRWLock(t *testing.T) {  cache := character03.New(100000000000)  num := 10000  var wg sync.WaitGroup  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Set("hello"+string(rune(index)), []byte("world"+string(rune(index))))  wg.Done() //5  }()  }   wg.Wait()  startTime := time.Now()  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Get("hello" + string(rune(index)))  wg.Done()  }()   }   wg.Wait()  endTime := time.Now()  fmt.Printf("stat:%v\n", cache.Stat())  // cost time:31.587204813s  fmt.Printf("cost time:%v\n", endTime.Sub(startTime))  
}  //cost time:12.012624518s  
func TestLock(t *testing.T) {  cache := lock.New(100000000000)  num := 10000  var wg sync.WaitGroup  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Set("hello"+string(rune(index)), []byte("world"+string(rune(index))))  wg.Done()  }()   }   wg.Wait()  startTime := time.Now()  for i := 0; i < num; i++ {  index := i  wg.Add(1) //  go func() {  cache.Get("hello" + string(rune(index)))  wg.Done() //5  }()  }   wg.Wait()  endTime := time.Now()  fmt.Printf("stat:%v\n", cache.Stat())  //cost time:31.765169643s  fmt.Printf("cost time:%v\n", endTime.Sub(startTime))  
}

3.4 自实现双向队列

使用原生的双向队列,确实可以省事,但是作为一个程序员,还是有一种造轮子的想法,那就自己写个双向队列,够用就行,不用实现所有的方法:

  
type List struct {  head *Node // 表头节点  tail *Node // 表尾节点  len  int   // 长度  
}  type Node struct {  next, prev *Node  list       *List  Value      any  
}  func NewList() *List {  return &List{}  
}  func NewNode(v any) *Node {  return &Node{  Value: v,  }}  func (l *List) Front() *Node {  return l.head  
}  func (l *List) Remove(node *Node) {  index := l.Find(node)  if index == -1 {  return  }  prev := node.prev  next := node.next  if prev == nil && next == nil {  l.len = 0  l.tail = nil  l.head = nil  return   }  if prev == nil {  next := node.next  next.prev = nil  l.head = next  l.len--  return  }  if next == nil {  prev := node.prev  prev.next = nil  l.tail = prev  l.len--  return  }  prev.next = next  next.prev = prev  l.len--  return  
}  func (l *List) Find(node *Node) int {  if l.len == 0 || node == nil {  return -1  }  index := 0  head := l.head  for head != nil {  if head != node {  head = head.next  index++  } else {  return index  }  }   return -1  
}  func (l *List) PushBack(v any) *Node {  node := NewNode(v)  if l.len == 0 {  node.prev = nil  node.next = nil  l.head = node  l.tail = node  } else {  tempTail := l.tail  tempTail.next = node  node.prev = tempTail  l.tail = node  }  l.len++  return node  
}

但凡使用到原生的 List 和 Node 的地方,都替换成自己实现的:

  
import (  "errors"  "log"   "sync")  type fifo struct {  // 为了快速找到该value  cache map[string]*Node  // 按照添加顺序保存的list  list *List  // 缓存数据  stat  Stat  mutex sync.RWMutex  
}  func (f *fifo) Set(key string, value []byte) {  f.mutex.Lock()  defer f.mutex.Unlock()  err := f.checkMaxBytes(key, value)  if err != nil {  log.Fatalf("checkMaxBytes err:%v\n", err)  return  }  if e, ok := f.cache[key]; ok && e != nil {  f.setDataExisted(e, value)  } else {  f.setDataNotExisted(key, value)  }}  func (f *fifo) checkMaxBytes(key string, value []byte) error {  if f.stat.maxBytes > 0 {  if f.stat.maxBytes < int64(len(key)+len(value)) {  return errors.New("over maxBytes")  }   }   return nil  
}  func (f *fifo) setDataNotExisted(key string, value []byte) {  newEntry := &entry{  key:   key,  value: value,  }   if f.stat.maxBytes > 0 {  for f.stat.usedBytes+int64(len(key)+len(value)) > f.stat.maxBytes {  f.removeOldestElement()  }   }   e := f.list.PushBack(newEntry)  f.cache[key] = e  f.stat.usedBytes = f.stat.usedBytes + int64(len(key)+len(value))  f.stat.keyCount++  }  func (f *fifo) setDataExisted(e *Node, value []byte) {  originEntry := e.Value.(*entry)  originSize := len(originEntry.value)  beRemoved := false  if f.stat.maxBytes > 0 {  for (int64)(len(value))-(int64)(originSize) > f.stat.maxBytes-f.stat.usedBytes {  if f.list.Front() == e {  beRemoved = true  }  f.removeOldestElement()  }   }   if beRemoved {  f.setDataNotExisted(originEntry.key, value)  return  }  originEntry.value = value  f.stat.usedBytes = f.stat.usedBytes + (int64)(len(value)) - (int64)(originSize)  }  func (f *fifo) removeOldestElement() {  f.removeElement(f.list.Front())  
}  func (f *fifo) removeElement(e *Node) {  if e == nil {  return  }  // 双向链表的删除  f.list.Remove(e)  originEntry := e.Value.(*entry)  f.stat.keyCount--  // 重新计算使用空间  f.stat.usedBytes = f.stat.usedBytes - int64(len(originEntry.key)+len(originEntry.value))  // Hash表删除  delete(f.cache, originEntry.key)  
}  func (f *fifo) Get(key string) []byte {  f.mutex.RLock()  defer f.mutex.RUnlock()  // time.Sleep(1000000)  if e, ok := f.cache[key]; ok {  return e.Value.(*entry).value  }  return nil  
}  
func (f *fifo) Del(key string) {  f.mutex.Lock()  defer f.mutex.Unlock()  if e, ok := f.cache[key]; ok {  f.removeElement(e)  }}  
func (f *fifo) Stat() Stat {  return f.stat  
}  func New(maxBytes int64) Cache {  return &fifo{  cache: map[string]*Node{},  list:  NewList(),  stat: Stat{  keyCount:  0,  maxBytes:  maxBytes,  usedBytes: 0,  },   }}

简单测试一下,符合预期:

func TestMyQueue(t *testing.T) {  Convey("TestApplyFunc", t, func() {  cache := character04.New(100)  stat := fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{0 100 0}")  cache.Set("hello", []byte("world"))  cache.Set("hello2", []byte("world2"))  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 22}")  cache.Set("hello2", []byte("changeWorld2"))  value := cache.Get("hello2")  So(string(value), ShouldEqual, "changeWorld2")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 28}")  cache.Set("k1", []byte("longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongV1"))  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 94}")  cache.Set("hello2", []byte("newHelloWorld2newHelloWorld2"))  value = cache.Get("hello2")  So(string(value), ShouldEqual, "newHelloWorld2newHelloWorld2")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{1 100 34}")  cache.Set("num", character01.IntToBytes(1))  num := cache.Get("num")  So(character01.BytesToInt(num), ShouldEqual, 1)  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 45}")  cache.Set("num", nil)  So(cache.Get("num"), ShouldEqual, nil)  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{2 100 37}")  cache.Del("num")  stat = fmt.Sprintf("stat:%v", cache.Stat())  So(stat, ShouldEqual, "stat:{1 100 34}")  })}

3.5 http 改造

一般缓存集成启动后,除了能使用代码直接操作,还会支持以 http 的方式操作,我们的也不能少了这个能力,那就来改造一下吧!

先封装一个 server, 监听 12345 端口,操作 Cache 和查询状态分别对应 cacheHandlerstatHandler

import (  "Go-Cache/00_fifo/character05"  "net/http")  type Server struct {  cache character05.Cache  
}  func New(c character05.Cache) *Server {  return &Server{  c,  }}  func (s *Server) Listen() {  // 注意是 /cache/  包括下面的子页面  http.Handle("/cache/", s.cacheHandler())  http.Handle("/stat", s.statHandler())  http.ListenAndServe(":12345", nil)  
}  func (s *Server) cacheHandler() *cacheHandler {  return &cacheHandler{  Server: s,  }}  func (s *Server) statHandler() *statHandler {  return &statHandler{  Server: s,  }}

状态查询的 handler 实现:

  
import (  "encoding/json"  "log"   "net/http")  type statHandler struct {  *Server  
}  func (h *statHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {  if r.Method != http.MethodGet {  w.WriteHeader(http.StatusMethodNotAllowed)  return  }  b, e := json.Marshal(h.cache.Stat())  if e != nil {  w.WriteHeader(http.StatusInternalServerError)  return  }  res, err := w.Write(b)  if err != nil {  log.Fatalln(err)  }   log.Printf(string(rune(res)))  
}

操作缓存 handler 的实现:

  
import (  "io/ioutil"  "net/http"   "strings")  type cacheHandler struct {  *Server  
}  func (h *cacheHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {  key := strings.Split(r.URL.EscapedPath(), "/")[2]  if len(key) == 0 {  w.WriteHeader(http.StatusBadRequest)  return  }  m := r.Method  if m == http.MethodGet {  b := h.cache.Get(key)  if len(b) == 0 {  w.WriteHeader(http.StatusNotFound)  return  }  w.Write(b)  return  }  if m == http.MethodPut {  b, _ := ioutil.ReadAll(r.Body)  if len(b) != 0 {  h.cache.Set(key, b)  }      return  }  if m == http.MethodDelete {  h.cache.Del(key)  return  }  w.WriteHeader(http.StatusMethodNotAllowed)  
}

启动类如下:

import (  "Go-Cache/00_fifo/character05"  "Go-Cache/00_fifo/character05/server")  func main() {  c := character05.New(0)  server.New(c).Listen()  
}

启动之后,我们打开 http://localhost:12345/stat, 可以看到以下内容,就是启动成功了:
image.png

image.png

下面是 curl 命令行操作:

 ~  curl -X PUT -d "world" http://localhost:12345/cache/hello~  curl http://localhost:12345/stat{"KeyCount":1,"MaxBytes":0,"UsedBytes":10}%~  curl http://localhost:12345/cache/helloworld%~  curl -X PUT -d "world2" http://localhost:12345/cache/hello~  curl http://localhost:12345/cache/helloworld2%~  curl -X PUT -d "smart" http://localhost:12345/cache/xiaoming~  curl http://localhost:12345/cache/xiaoming
smart%

image.png

4 小结一下

总的来说,FIFO 是极其简单的缓存淘汰算法,思路很清晰,初学者可以试试,它并不高效,但是它是最简单粗暴的处理方法,先进先出,不会区别对待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/843173.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安装在C盘的软件移到其它盘

移动Office到D盘为例,Office默认安装位置为"C:Program FilesMicrosoft Office",只需要把“Microsoft Office”这个目录移动到C盘以外的盘符,我们这里以D盘为例。1)在D盘新建一个文件夹“Program Files”,然后按“Shift键不放,点重启电脑”进入安全模式;2)在安…

HCIP-01 RSTP原理与配置

生成树是一个用于局域网中消除环路的协议。运行该协议的设备通过彼此交互信息而发现网络中的环路,并对某些接口进行阻塞以消除环路。由于局域网规模的不断增长,生成树协议已经成为重要的局域网协议之一。 RSTP是生成树协议中的其中一个版本,它在STP的基础上,做了很多的改进…

Ripro V5日主题 v8.3 开心授权版 wordpress主题虚拟资源下载站首选主题模板

简介: RiPro主题全新V5版本,是一个优秀且功能强大、易于管理、现代化的WordPress虚拟资源商城主题。支持首页模块化布局和WP原生小工具模块化首页可拖拽设置,让您的网站设计体验更加舒适。同时支持了高级筛选、自带会员生态系统、超全支付接口等众多功能,无需依赖插件即可实…

从“三好”到“超三好”,智界新S7“卷”回来了!

11月26日,鸿蒙智行旗下智界新S7在华为Mate品牌盛典上市,并公布正式售价。智界新S7正式上市推出3款车型,智界新S7 Pro售价22.98万元,智界新S7 Max售价26.98万元起,智界新S7 Ultra售价31.98万元,首销期即刻下定,至高可享受价值35000元的权益。智界新S7将于12月1日开启交付…

yolo --- 模型训练

文件存放路径数据集标注软件pip install labelimg安装好后启动 labelimgOpen Dir:打开需要标注的图片路径 Change Save Dir:标注文件保存路径 View--->Auto Save mode:把这个勾上就不会每到下一张询问你一次按w可以显示画框的坐标十字 按D键可以切换到下一张 注意标注的标…

Linux物理内存管理

1 物理内存初始化——引导分配器memblockLinux内核启动时,先要初始化物理内存,这个阶段的作用主要是确定物理内存大小,哪些是可用的?哪些是预留的?完成这一阶段工作的是memblock引导分配器。内核启动时初始化物理内存的处理函数调用路径大概是(基于Linux 5.10.1源码查看):…

Threejs入门-灯光

在 Three.js 中,灯光是非常重要的元素之一,它能够模拟现实世界中的光照效果,帮助我们打造更加真实的三维场景。灯光的种类和配置方式可以影响整个场景的视觉效果,在不同的应用中,灯光的使用非常关键。 灯光概述 Three.js 提供了几种常见的光源类型:环境光(Ambient Light…

《Django 5 By Example》阅读笔记:p383-p387

《Django 5 By Example》学习第 14 天,p383-p387 总结,总计 5 页。 一、技术总结 1. asynchronous task(异步任务) 对于异步任务,书中使用的是 celery 和 RabbitMQ,这也是平时工作中的主流用法。 (1) celery Python 使用的 celery 包的名字也是 celery。 2. RabbitMQ (1) 拉…

思维模型:六顶思考帽,从不同角度思考问题,提升思维认知

思维模型:六顶思考帽思维模型,从不同角度思考分析问题,提升思维认知介绍 六顶思考帽(Six Thinking hats) ,由爱德华德博诺博士提出的一种思维认知模型工具。 它是通过不同颜色的帽子来代表六种不同的思维方式,帮助人们从多个角度全面分析和解决问题,提升认知。这六种颜…

12.SpringCloudAlibabaSentinel实现熔断和限流

1.Sentinel 1.1 官网 sentinel官网,类似Spring Cloud Circuit Breaker。 1.2 是什么 面向分布式、多语言异构化服务架构的流量治理组件。 1.3 下载地址 https://github.com/alibaba/Sentinel/releases 1.4 能干吗 Sentinel以流量为切入点,从流量控制、流量路由、熔断降级、系…

视觉多模态理解模型基础

1. 视觉多模态简介 视觉多模态一般涵盖2个要点:视觉表征 以及 视觉与自然语言的对齐(Visual Language Alignment)或融合。1.1. 视觉表征 视觉表征是指:将图像信息转化为深度学习模型可以处理的特征向量或向量序列的过程。具体来说,它包含以下2点:合理建模视觉输入特征:这…

Padavan 使用VNT

openwrt ppoe拨号有问题,实在忍受不了,只能换回稳定的Padavan下载二进制vnt放至/etc/storage添加自启脚本/etc/storage/vnt-cli -s tcp://x.x.x.x:29872 -k passwd --use-channel relay -n R3P -o 192.168.123.0/24 -i 10.1.50.0/24,10.1.1.4 -i 10.2.110.0/24,10.1.1.223 …