freecache

一句话描述

Go缓存库,具有零GC开销和高并发性能

简介

freecache是什么?

使用FreeCache,您可以在内存中缓存无限数量的对象,而不会增加延迟和降低吞吐量。

为什么选择freecache?

  • 支持存储大量数据条目
  • 零 GC
  • 协程安全访问
  • 过期时间支持
  • 接近LRU的淘汰算法
  • 严格的内存使用
  • 迭代器支持

性能如何

下面基准测试与内置Map的比较结果,“Set”性能比内置Map快约2倍,“Get”性能比内置Map慢约1/2倍。 由于它是单线程基准测试,因此在多线程环境中,FreeCache应该比单锁保护的内置Map快许多倍。

  1. BenchmarkCacheSet 3000000 446 ns/op
  2. BenchmarkMapSet 2000000 861 ns/op
  3. BenchmarkCacheGet 3000000 517 ns/op
  4. BenchmarkMapGet 10000000 212 ns/op

Example

  1. package main
  2. import (
  3. "fmt"
  4. "runtime/debug"
  5. "github.com/coocood/freecache"
  6. )
  7. func main() {
  8. // 缓存大小,100M
  9. cacheSize := 100 * 1024 * 1024
  10. cache := freecache.NewCache(cacheSize)
  11. debug.SetGCPercent(20)
  12. key := []byte("abc")
  13. val := []byte("def")
  14. expire := 60 // expire in 60 seconds
  15. // 设置KEY
  16. cache.Set(key, val, expire)
  17. got, err := cache.Get(key)
  18. if err != nil {
  19. fmt.Println(err)
  20. } else {
  21. fmt.Printf("%s\n", got)
  22. }
  23. fmt.Println("entry count ", cache.EntryCount())
  24. affected := cache.Del(key)
  25. fmt.Println("deleted key ", affected)
  26. fmt.Println("entry count ", cache.EntryCount())
  27. }

freecache 有几个特点:零 GC接近LRU的淘汰算法,迭代器支持
我们将从源码分析、核心的存储结构来分析他是怎么实现的

核心的存储结构

  1. package freecache
  2. import (
  3. "sync"
  4. )
  5. const (
  6. segmentCount = 256
  7. )
  8. // Cache is a freecache instance.
  9. type Cache struct {
  10. locks [segmentCount]sync.Mutex
  11. segments [segmentCount]segment // 256个 segment, segment实际存储数据的结构
  12. // segment 里面有一个 环形数组,环形数组的大小按照 size/segmentCount 来确定
  13. }
  14. type segment struct {
  15. rb RingBuf // 实际数据存储的数组
  16. slotsData []entryPtr // 数据索引地址,用于定位到具体的数据在数组中的位置
  17. ...
  18. }
  19. // entryPtr 索引
  20. type entryPtr struct {
  21. offset int64 // 数据在环形数组中的偏移量
  22. hash16 uint16
  23. keyLen uint16
  24. reserved uint32
  25. }
  26. // RingBuf 存储实际数据
  27. type RingBuf struct {
  28. begin int64
  29. end int64
  30. data []byte
  31. index int
  32. }
  33. // RingBuf 中的数据头, 这个记录的是
  34. type entryHdr struct {
  35. accessTime uint32
  36. expireAt uint32
  37. keyLen uint16
  38. hash16 uint16
  39. valLen uint32
  40. valCap uint32
  41. deleted bool
  42. slotId uint8
  43. reserved uint16
  44. }
  • 可以看到freecache将实际存储数据结构设计为数组,有segmentCount即256个segment和互斥锁,这样锁的粒度就相对较小,从而减小了资源竞争。
  • freecache 减少了指针的使用,所以freecache的对GC开销几乎为零。

结构图

每日一库之85:freecache - 图1

流程分析

流程分析只分析了了Set和淘汰算法的实现

Set

  1. Set数据是会先计算出Key对应的hash值,这个数据会在后面计算segID和slotId使用到
  2. 做一些数据合法的判断
  3. 根据slotId找到slot
  4. 根据slot和hash16获取到在Key在slot中的index
  5. 拿到index之后在cache的RingBuf中查找数据
  6. 找到的时候则需要根据新旧两个数据长度判断是否需要扩容
  7. 没有找到则直接写入新的数据
    每日一库之85:freecache - 图2

淘汰算法的实现

freecache 的淘汰算法有两种实现:过期删除、接近LRU的淘汰算法

过期删除

freecache的过期删除并不是有一个后台协程去删除,而是在Get的时候才会判断,这样可以减少锁的抢占

  1. package freecache
  2. // 发现是过期直接就返回ErrNotFound
  3. if hdr.expireAt != 0 && hdr.expireAt <= now {
  4. seg.delEntryPtr(slotId, slot, idx)
  5. atomic.AddInt64(&seg.totalExpired, 1)
  6. err = ErrNotFound
  7. atomic.AddInt64(&seg.missCount, 1)
  8. return
  9. }

接近LRU的淘汰算法

  1. package freecache
  2. // 如果过期
  3. expired := oldHdr.expireAt != 0 && oldHdr.expireAt < now
  4. // 近似LRU
  5. leastRecentUsed := int64(oldHdr.accessTime)*atomic.LoadInt64(&seg.totalCount) <= atomic.LoadInt64(&seg.totalTime)
  6. if expired || leastRecentUsed || consecutiveEvacuate > 5 {
  7. seg.delEntryPtrByOffset(oldHdr.slotId, oldHdr.hash16, oldOff)
  8. if oldHdr.slotId == slotId {
  9. slotModified = true
  10. }
  11. consecutiveEvacuate = 0
  12. atomic.AddInt64(&seg.totalTime, -int64(oldHdr.accessTime))
  13. atomic.AddInt64(&seg.totalCount, -1)
  14. seg.vacuumLen += oldEntryLen
  15. if expired {
  16. atomic.AddInt64(&seg.totalExpired, 1)
  17. } else {
  18. atomic.AddInt64(&seg.totalEvacuate, 1)
  19. }
  • oldHdr.accessTime:entry最近一次访问的时间戳。
  • seg.totalCount:RingBuffer中entry的总数,包括过期和标记删除的entry。
  • seg.totalTime:RingBuffer中每个entry最近一次访问的时间戳总和。
  • 所以 atomic.LoadInt64(&seg.totalTime)/atomic.LoadInt64(&seg.totalCount)表示RingBuf中的entry最近一次访问时间戳的平均值,
    当一个entry的accessTime小于等于这个平均值,则认为这个entry是可以被置换掉的。

Doc

http://godoc.org/github.com/coocood/freecache

比较

相似的库