深入理解go缓存库freecache的使用
来源:脚本之家
时间:2022-12-23 19:51:35 332浏览 收藏
IT行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰。所以我们需要踏踏实实的不断学习,精进自己的技术,尤其是初学者。今天golang学习网给大家整理了《深入理解go缓存库freecache的使用》,聊聊gofreecache,我们一起来看看吧!
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; i1 初始化
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; i2 读写流程
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) hash16 := uint16(hashVal >> 16) slot := seg.getSlot(slotId) idx, match := seg.lookup(slot, hash16, 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.hash16 = hash16 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 uint32(maxKeyValLen-len(key)) { hdr.valCap = uint32(maxKeyValLen - len(key)) } } else { // 无数据 hdr.slotId = slotId hdr.hash16 = hash16 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, hash16, key) // assert(match == false) } newOff := seg.rb.End() seg.insertEntryPtr(slotId, hash16, 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 5 { // 可以回收 seg.delEntryPtrByOffset(oldHdr.slotId, oldHdr.hash16, 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.hash16, 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数据的复制操作。
3 总结
从常见的几个缓存框架压测性能来看,Set性能差异较大但还不是数量级别的差距,Get性能差异不大,因此对于绝大多数场景来说不用太关注Set/Get性能,重点应该看功能是否满足业务需求和gc影响,性能压测比较见:https://golang2.eddycjy.com/posts/ch5/04-performance/
缓存有一个特殊的场景,那就是将数据全部缓存在内存,涉及到更新时都是全量更新(替换),该场景下使用freecache,如果size未设置好可能导致部分数据被淘汰,是不符合预期的,这个一定要注意。为了使用freecache避免该问题,需要将size设置"足够大",但也要注意其内存空间占用。
本篇关于《深入理解go缓存库freecache的使用》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于Golang的相关知识,请关注golang学习网公众号!
-
505 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
216 收藏
-
284 收藏
-
103 收藏
-
315 收藏
-
218 收藏
-
185 收藏
-
100 收藏
-
222 收藏
-
111 收藏
-
125 收藏
-
474 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 508次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 满意的老师
- 很有用,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢大佬分享文章!
- 2023-02-14 21:15:39
-
- 文艺的西装
- 这篇技术文章真是及时雨啊,细节满满,感谢大佬分享,已加入收藏夹了,关注楼主了!希望楼主能多写Golang相关的文章。
- 2023-01-28 13:03:59
-
- 粗暴的帆布鞋
- 太细致了,码住,感谢大佬的这篇技术贴,我会继续支持!
- 2023-01-18 01:17:27
-
- 风趣的项链
- 这篇技术贴出现的刚刚好,大佬加油!
- 2023-01-09 14:59:00
-
- 欣慰的背包
- 太给力了,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢作者分享技术贴!
- 2023-01-01 06:57:42
-
- 聪慧的钢笔
- 这篇技术文章出现的刚刚好,太全面了,写的不错,mark,关注up主了!希望up主能多写Golang相关的文章。
- 2022-12-24 18:43:28