千家信息网

Golang中的缓存库freecache怎么用

发表于:2025-11-14 作者:千家信息网编辑
千家信息网最后更新 2025年11月14日,这篇文章主要讲解了"Golang中的缓存库freecache怎么用",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Golang中的缓存库freecach
千家信息网最后更新 2025年11月14日Golang中的缓存库freecache怎么用

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

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

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

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

  • 零GC开销:比如freecache或bigcache这种,底层基于ringbuf,减小指针个数;

  • 有GC开销:直接基于Map来实现的缓存框架。

对于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怎么用这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

数据 缓存 指针 类型 空间 大小 就是 框架 流程 索引 存库 内存 序列 影响 更新 场景 数组 存储 学习 下标 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 我的世界蛤蟆吃服务器 2021网络安全空间考研 黑龙江省网络安全中心 网络安全堆溢出实验 监听数据库数据变化 分析 望谟县天气预报软件开发 辽宁调度服务器品牌 银行软件开发中心都考什么 上海融腾集团软件开发 光遇是所有人在同一个服务器的吗 贵州服务器散热风扇生产厂家 美国前十家网络安全公司 人岁的软件开发 服务器程序加密 网络技术智能大厦 软件开发计量单位 观看网络安全宣传片心得体会 绱佳网络技术有限公司 数据库系统数据共享指的是 网络安全关键技术主要涉及 数据库与系统字符集不同 男人人手机号码数据库 建立自己的云储存服务器 南京网络安全技术培训课程 湖北网络技术专升本考几门课 网络安全行业可以干到退休吗 网络安全法十四条 数据库怎样备份与还原 云南时代网络技术服务产品介绍 长宁区项目数据库服务商销售价格
0