文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Golang中的缓存库freecache怎么用

2023-06-29 06:34

关注

这篇文章主要讲解了“Golang中的缓存库freecache怎么用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang中的缓存库freecache怎么用”吧!

Golang中的缓存库freecache怎么用

go开发缓存场景一般使用map或者缓存框架,为了线程安全会使用sync.Map或线程安全的缓存框架。

缓存场景中如果数据量大于百万级别,需要特别考虑数据类型对于gc的影响(注意string类型底层是指针+Len+Cap,因此也算是指针类型),如果缓存key和value都是非指针类型的话就无需多虑了。

但实际应用场景中,key和value是(包含)指针类型数据是很常见的,因此使用缓存框架需要特别注意其对gc影响,从是否对GC影响角度来看缓存框架大致分为2类:

对于map而言,gc时会扫描所有key/value键值对,如果其都是基本类型,那么gc便不会再扫描。

下面以freecache为例分析下其实现原理,代码示例如下:

func main() {   cacheSize := 100 * 1024 * 1024   cache := freecache.NewCache(cacheSize)   for i := 0; i < N; i++ {      str := strconv.Itoa(i)      _ = cache.Set([]byte(str), []byte(str), 1)   }   now := time.Now()   runtime.GC()   fmt.Printf("freecache, GC took: %s\n", time.Since(now))   _, _ = cache.Get([]byte("aa"))   now = time.Now()   for i := 0; i < N; i++ {      str := strconv.Itoa(i)      _, _ = cache.Get([]byte(str))   }   fmt.Printf("freecache, Get took: %s\n\n", time.Since(now))}

1 初始化

freecache.NewCache会初始化本地缓存,size表示存储空间大小,freecache会初始化256个segment,每个segment是独立的存储单元,freecache加锁维度也是基于segment的,每个segment有一个ringbuf,初始大小为size/256。freecache号称零GC的来源就是其指针是固定的,只有512个,每个segment有2个,分别是rb和slotData(注意切片为指针类型)。

type segment struct {   rb            RingBuf // ring buffer that stores data   segId         int   _             uint32  // 占位   missCount     int64   hitCount      int64   entryCount    int64   totalCount    int64      // number of entries in ring buffer, including deleted entries.   totalTime     int64      // used to calculate least recent used entry.   timer         Timer      // Timer giving current time   totalEvacuate int64      // used for debug   totalExpired  int64      // used for debug   overwrites    int64      // used for debug   touched       int64      // used for debug   vacuumLen     int64      // up to vacuumLen, new data can be written without overwriting old data.   slotLens      [256]int32 // The actual length for every slot.   slotCap       int32      // max number of entry pointers a slot can hold.   slotsData     []entryPtr // 索引指针}func NewCacheCustomTimer(size int, timer Timer) (cache *Cache) {    cache = new(Cache)    for i := 0; i < segmentCount; i++ {        cache.segments[i] = newSegment(size/segmentCount, i, timer)    }}func newSegment(bufSize int, segId int, timer Timer) (seg segment) {    seg.rb = NewRingBuf(bufSize, 0)    seg.segId = segId    seg.timer = timer    seg.vacuumLen = int64(bufSize)    seg.slotCap = 1    seg.slotsData = make([]entryPtr, 256*seg.slotCap) // 每个slotData初始化256个单位大小}

2 读写流程

freecache的key和value都是[]byte数组,使用时需要自行序列化和反序列化,如果缓存复杂对象不可忽略其序列化和反序列化带来的影响,首先看下Set流程:

_ = cache.Set([]byte(str), []byte(str), 1)

Set流程首先对key进行hash,hashVal类型uint64,其低8位segID对应segment数组,低8-15位表示slotId对应slotsData下标,高16位表示slotsData下标对应的[]entryPtr某个数据,这里需要查找操作。注意[]entryPtr数组大小为slotCap(初始为1),当扩容时会slotCap倍增。

每个segment对应一个lock(sync.Mutex),因此其能够支持较大并发量,而不像sync.Map只有一个锁。

func (cache *Cache) Set(key, value []byte, expireSeconds int) (err error) {   hashVal := hashFunc(key)   segID := hashVal & segmentAndOpVal // 低8位   cache.locks[segID].Lock() // 加锁   err = cache.segments[segID].set(key, value, hashVal, expireSeconds)   cache.locks[segID].Unlock()}func (seg *segment) set(key, value []byte, hashVal uint64, expireSeconds int) (err error) {   slotId := uint8(hashVal >> 8)   hash26 := uint16(hashVal >> 16)   slot := seg.getSlot(slotId)   idx, match := seg.lookup(slot, hash26, key)   var hdrBuf [ENTRY_HDR_SIZE]byte   hdr := (*entryHdr)(unsafe.Pointer(&hdrBuf[0]))   if match { // 有数据更新操作      matchedPtr := &slot[idx]      seg.rb.ReadAt(hdrBuf[:], matchedPtr.offset)      hdr.slotId = slotId      hdr.hash26 = hash26      hdr.keyLen = uint16(len(key))      originAccessTime := hdr.accessTime      hdr.accessTime = now      hdr.expireAt = expireAt      hdr.valLen = uint32(len(value))      if hdr.valCap >= hdr.valLen {         // 已存在数据value空间能存下此次value大小         atomic.AddInt64(&seg.totalTime, int64(hdr.accessTime)-int64(originAccessTime))         seg.rb.WriteAt(hdrBuf[:], matchedPtr.offset)         seg.rb.WriteAt(value, matchedPtr.offset+ENTRY_HDR_SIZE+int64(hdr.keyLen))         atomic.AddInt64(&seg.overwrites, 1)         return      }      // 删除对应entryPtr,涉及到slotsData内存copy,ringbug中只是标记删除      seg.delEntryPtr(slotId, slot, idx)      match = false      // increase capacity and limit entry len.      for hdr.valCap < hdr.valLen {         hdr.valCap *= 2      }      if hdr.valCap > uint32(maxKeyValLen-len(key)) {         hdr.valCap = uint32(maxKeyValLen - len(key))      }   } else { // 无数据      hdr.slotId = slotId      hdr.hash26 = hash26      hdr.keyLen = uint16(len(key))      hdr.accessTime = now      hdr.expireAt = expireAt      hdr.valLen = uint32(len(value))      hdr.valCap = uint32(len(value))      if hdr.valCap == 0 { // avoid infinite loop when increasing capacity.         hdr.valCap = 1      }   }      // 数据实际长度为 ENTRY_HDR_SIZE=24 + key和value的长度       entryLen := ENTRY_HDR_SIZE + int64(len(key)) + int64(hdr.valCap)   slotModified := seg.evacuate(entryLen, slotId, now)   if slotModified {      // the slot has been modified during evacuation, we need to looked up for the 'idx' again.      // otherwise there would be index out of bound error.      slot = seg.getSlot(slotId)      idx, match = seg.lookup(slot, hash26, key)      // assert(match == false)   }   newOff := seg.rb.End()   seg.insertEntryPtr(slotId, hash26, newOff, idx, hdr.keyLen)   seg.rb.Write(hdrBuf[:])   seg.rb.Write(key)   seg.rb.Write(value)   seg.rb.Skip(int64(hdr.valCap - hdr.valLen))   atomic.AddInt64(&seg.totalTime, int64(now))   atomic.AddInt64(&seg.totalCount, 1)   seg.vacuumLen -= entryLen   return}

seg.evacuate会评估ringbuf是否有足够空间存储key/value,如果空间不够,其会从空闲空间尾部后一位(也就是待淘汰数据的开始位置)开始扫描(oldOff := seg.rb.End() + seg.vacuumLen - seg.rb.Size()),如果对应数据已被逻辑deleted或者已过期,那么该块内存可以直接回收,如果不满足回收条件,则将entry从环头调换到环尾,再更新entry的索引,如果这样循环5次还是不行,那么需要将当前oldHdrBuf回收以满足内存需要。

执行完seg.evacuate所需空间肯定是能满足的,然后就是写入索引和数据了,insertEntryPtr就是写入索引操作,当[]entryPtr中元素个数大于seg.slotCap(初始1)时,需要扩容操作,对应方法见seg.expand,这里不再赘述。

写入ringbuf就是执行rb.Write即可。

func (seg *segment) evacuate(entryLen int64, slotId uint8, now uint32) (slotModified bool) {   var oldHdrBuf [ENTRY_HDR_SIZE]byte   consecutiveEvacuate := 0   for seg.vacuumLen < entryLen {      oldOff := seg.rb.End() + seg.vacuumLen - seg.rb.Size()      seg.rb.ReadAt(oldHdrBuf[:], oldOff)      oldHdr := (*entryHdr)(unsafe.Pointer(&oldHdrBuf[0]))      oldEntryLen := ENTRY_HDR_SIZE + int64(oldHdr.keyLen) + int64(oldHdr.valCap)      if oldHdr.deleted { // 已删除         consecutiveEvacuate = 0         atomic.AddInt64(&seg.totalTime, -int64(oldHdr.accessTime))         atomic.AddInt64(&seg.totalCount, -1)         seg.vacuumLen += oldEntryLen         continue      }      expired := oldHdr.expireAt != 0 && oldHdr.expireAt < now      leastRecentUsed := int64(oldHdr.accessTime)*atomic.LoadInt64(&seg.totalCount) <= atomic.LoadInt64(&seg.totalTime)      if expired || leastRecentUsed || consecutiveEvacuate > 5 {      // 可以回收         seg.delEntryPtrByOffset(oldHdr.slotId, oldHdr.hash26, oldOff)         if oldHdr.slotId == slotId {            slotModified = true         }         consecutiveEvacuate = 0         atomic.AddInt64(&seg.totalTime, -int64(oldHdr.accessTime))         atomic.AddInt64(&seg.totalCount, -1)         seg.vacuumLen += oldEntryLen         if expired {            atomic.AddInt64(&seg.totalExpired, 1)         } else {            atomic.AddInt64(&seg.totalEvacuate, 1)         }      } else {         // evacuate an old entry that has been accessed recently for better cache hit rate.         newOff := seg.rb.Evacuate(oldOff, int(oldEntryLen))         seg.updateEntryPtr(oldHdr.slotId, oldHdr.hash26, oldOff, newOff)         consecutiveEvacuate++         atomic.AddInt64(&seg.totalEvacuate, 1)      }   }}

freecache的Get流程相对来说简单点,通过hash找到对应segment,通过slotId找到对应索引slot,然后通过二分+遍历寻找数据,如果找不到直接返回ErrNotFound,否则更新一些time指标。Get流程还会更新缓存命中率相关指标。

func (cache *Cache) Get(key []byte) (value []byte, err error) {   hashVal := hashFunc(key)   segID := hashVal & segmentAndOpVal   cache.locks[segID].Lock()   value, _, err = cache.segments[segID].get(key, nil, hashVal, false)   cache.locks[segID].Unlock()   return}func (seg *segment) get(key, buf []byte, hashVal uint64, peek bool) (value []byte, expireAt uint32, err error) {   hdr, ptr, err := seg.locate(key, hashVal, peek) // hash+定位查找   if err != nil {      return   }   expireAt = hdr.expireAt   if cap(buf) >= int(hdr.valLen) {      value = buf[:hdr.valLen]   } else {      value = make([]byte, hdr.valLen)   }   seg.rb.ReadAt(value, ptr.offset+ENTRY_HDR_SIZE+int64(hdr.keyLen))}

定位到数据之后,读取ringbuf即可,注意一般来说读取到的value是新创建的内存空间,因此涉及到[]byte数据的复制操作。

感谢各位的阅读,以上就是“Golang中的缓存库freecache怎么用”的内容了,经过本文的学习后,相信大家对Golang中的缓存库freecache怎么用这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯