この記事では、Golang キャッシュについて理解し、Golang のキャッシュ ライブラリ freecache を簡単に紹介します。
# Go 開発キャッシュ シナリオでは、通常、マップまたはキャッシュ フレームワークが使用されます。スレッド セーフのために、sync.Map
またはスレッド セーフ キャッシュ フレームワークが使用されます。
キャッシュ シナリオ内のデータ量が 100 万レベルを超える場合は、gc に対するデータ型の影響を特別に考慮する必要があります (文字列型の最下層はポインターであることに注意してください) Len Cap なのでポインタ型でもあります) キャッシュキーと値が両方とも非 For ポインタ型の場合は、心配する必要はありません。 [関連する推奨事項: Go ビデオ チュートリアル ]
しかし、実際のアプリケーション シナリオでは、キーと値がポインター型データ (を含む) であることが非常に一般的であるため、キャッシュ フレームワークを使用する場合は、使用時の GC への影響には特別な注意を払う必要があります。GC に影響を与えるかどうかの観点から、キャッシュ フレームワークは大きく 2 つのカテゴリに分類されます。
map の場合、gc 中にすべてのキーと値のペアがスキャンされます。すべてが基本タイプの場合、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)) }
freecache.NewCache は、ローカル キャッシュ、サイズはストレージを表します スペース サイズに関して、freecache は 256 セグメントを初期化します。各セグメントは独立したストレージ ユニットです。freecache のロック ディメンションもセグメントに基づいています。各セグメントには、初期サイズが size/ のリングバッファがあります。 256. freecache が GC がゼロであると主張する理由は、そのポインターが 512 個のみで固定されており、各セグメントが 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个单位大小 }
freecache のキーと値は両方とも []byte
配列です。使用する場合は自分でシリアル化および逆シリアル化する必要があります。複雑なオブジェクトをキャッシュする シリアル化と逆シリアル化の影響は無視できません。まず、Set
プロセスを見てください:
_ = cache.Set([]byte(str), []byte(str), 1)
Set プロセスは最初にキーをハッシュし、hashVal タイプは uint64 であり、その低セグメント配列に対応する 8 ビット segID、下位 8 ~ 15 ビットは、slotId がslotsData 添字に対応することを示し、上位 16 ビットは、slotsData 添字に対応する []entryPtr
特定のデータを示します。ここでは検索操作が必要です。なお、[]entryPtr
配列のサイズはslotCap(初期値は1)で、容量を拡張するとslotCapは2倍になります。
各セグメントはロック (sync.Mutex) に対応するため、ロックが 1 つしかない 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 < hdr.valLen { hdr.valCap *= 2 } if 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 にキー/値を保存するのに十分な領域があるかどうかを評価します。十分な領域がない場合は、空き領域の終わり (つまり、開始領域) からスキャンを開始します。 (oldOff := seg.rb.End() seg.vacuumLen - seg.rb.Size()
)、対応するデータが論理的に削除されているか期限切れの場合、メモリのブロックは直接リサイクルできます。リサイクル条件が満たされない場合、エントリはリングの先頭からリングの末尾までスワップされ、その後エントリのインデックスが更新されます。サイクルが 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.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 プロセスは比較的単純です。ハッシュを通じて対応するセグメントを見つけ、slotId を通じて対応するインデックス スロットを見つけ、バイナリ トラバーサルを通じてデータを検索します。見つからない場合は直接 ErrNotFound を返し、それ以外の場合は更新しますしばらくの間インジケーターです。 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 を読み取るだけです。通常、読み取られる値は新しく作成されたメモリ空間であるため、[]byte
データのコピー操作が含まれることに注意してください。
いくつかの一般的なキャッシュ フレームワークのストレス テストのパフォーマンスから判断すると、Set のパフォーマンスの差は大きいですが、定量的なレベルではありません。Get のパフォーマンスの差は大きくないため、ほとんどのキャッシュ フレームワークでは、ほとんどのシナリオでは、Set/Get のパフォーマンスにあまり注意を払う必要はありません。関数がビジネス ニーズと GC の影響を満たしているかどうかに重点を置く必要があります。パフォーマンス ストレス テストの比較については、https://golang2 を参照してください。 .eddycjy.com/posts/ch5/04-performance/
キャッシュには特別なシナリオがあり、すべてのデータをメモリにキャッシュし、更新に関しては完全に更新 (置換) されます。このシナリオではフリーキャッシュを使用していますが、サイズが適切に設定されていない場合、一部のデータが削除される可能性があり、期待どおりではありませんので注意が必要です。この問題を回避するためにフリーキャッシュを使用するには、サイズを「十分な大きさ」に設定する必要がありますが、そのメモリ領域の使用量にも注意する必要があります。
プログラミング関連の知識について詳しくは、プログラミング教育をご覧ください。 !
以上がGolang のキャッシュ ライブラリ freecache について学ぶための 1 つの記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。