Golang本地缓存利器fastcache一文学透

2023-07-0216:53:29后端程序开发Comments858 views字数 4108阅读模式

一、介绍文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

fastcache是一个用go语言实现的,很快的,线程安全的,内存缓存的,用于大量对象缓存的组件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

它的特点是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  1. 快速的多核CPU的性能可扩展。
  2. 线程安全。并发goroutines可以读取和写入单个缓存实例。
  3. 快速缓存是为存储大量条目而设计的,没有GC开销。
  4. 当达到创建时设置的最大缓存大小时,Fastcache会自动收回旧条目。
  5. 简单的API
  6. 简单的源代码。
  7. 缓存可以保存到文件中,也可以从文件中加载。
  8. 适用于谷歌应用引擎。

二、性能文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

到底有多快?:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

GOMAXPROCS=4 go test github.com/VictoriaMetrics/fastcache -bench='Set|Get' -benchtime=10sgoos: linuxgoarch: amd64pkg: github.com/VictoriaMetrics/fastcacheBenchmarkBigCacheSet-4            2000    10566656 ns/op     6.20 MB/s   4660369 B/op         6 allocs/opBenchmarkBigCacheGet-4            2000     6902694 ns/op     9.49 MB/s    684169 B/op    131076 allocs/opBenchmarkBigCacheSetGet-4         1000    17579118 ns/op     7.46 MB/s   5046744 B/op    131083 allocs/opBenchmarkCacheSet-4               5000     3808874 ns/op    17.21 MB/s      1142 B/op         2 allocs/opBenchmarkCacheGet-4               5000     3293849 ns/op    19.90 MB/s      1140 B/op         2 allocs/opBenchmarkCacheSetGet-4            2000     8456061 ns/op    15.50 MB/s      2857 B/op         5 allocs/opBenchmarkStdMapSet-4              2000    10559382 ns/op     6.21 MB/s    268413 B/op     65537 allocs/opBenchmarkStdMapGet-4              5000     2687404 ns/op    24.39 MB/s      2558 B/op        13 allocs/opBenchmarkStdMapSetGet-4            100   154641257 ns/op     0.85 MB/s    387405 B/op     65558 allocs/opBenchmarkSyncMapSet-4              500    24703219 ns/op     2.65 MB/s   3426543 B/op    262411 allocs/opBenchmarkSyncMapGet-4             5000     2265892 ns/op    28.92 MB/s      2545 B/op        79 allocs/opBenchmarkSyncMapSetGet-4          1000    14595535 ns/op     8.98 MB/s   3417190 B/op    262277 allocs/op

将Fastcache的性能与BigCache、标准Go映射和sync.map进行比较。可以看出来FashCache的写入效率最快。换个角度说明一下就是fastcache比标准map快2.77倍,比sync.Map库快6.49倍,比BigCache快2.78倍。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

三、限制文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

当然fastcache并不是完美的,也是存在缺陷的,可以根据下面限制决定是否要引入自己的业务环境:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  • key和value都只能是[]byte类型,不是的话要自己序列化
  • 大小超过64KB的大条目必须通过Cache.SetBig来存储
  • 没有缓存过期。只有在缓存大小溢出时,才会从缓存中逐出条目。输入截止日期以存储在值内,以便实现缓存过期。

四、源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

Golang本地缓存利器fastcache一文学透文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

4.1 使用mmap分配内存

malloc_mmap.go中使用了unix.Mmap()来分配内存:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  • 内存映射的方式可以直接向操作系统申请内存,这块区域不归GC管。所以不管你在这块内存缓存了多少数据,都不会因为GC扫描而影响性能。
  • 每次使用mmap申请内存的时候,申请了1024*64KB=64MB内存。
每64KB称为一个chunk所有的chunk放在一个队列中当队列中所有的chunk都用完后,再申请64MB
  • chunk的管理:
var (      freeChunks     []*[chunkSize]byte  //相当于一个队列,保存了所有未使用的chunk      freeChunksLock sync.Mutex  //chunk的锁)

可以通过 func getChunk() []byte 函数获取一个64KB的块。如果freeChunks中没有chunk了,就再通过mmap申请64MB。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

chunk的归还func putChunk(chunk []byte) 函数把有效的chunk放回freeChunks队列。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

绕过GC能带来性能上的好处,但是这里分配的内存再也不会被释放,直到进程重启。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

4.2 Cache类实现

fastcache.go中是fastcache的主要代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

4.2.1 Cache对象的结构
type Cache struct {      buckets [bucketsCount]bucket
      bigStats BigStats}
  • bucketsCount这个常量值为512 。也就是说,cache对象的内部分布了512个桶。
  • bigStats 是用于内部的监控上报的

4.2.2 新建Cache对象文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

// func New(maxBytes int) *Cachec := New(1024*1024*32)  //cache的最小容量是32MB

New源码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

func New(maxBytes int) *Cache {      if maxBytes <= 0 {            panic(fmt.Errorf("maxBytes must be greater than 0; got %d", maxBytes))  }      var c Cache      maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount)      for i := range c.buckets[:] {            c.buckets[i].Init(maxBucketBytes)  }      return &c}
  • maxBytes先按照512字节向上对齐
  • 然后划分成512份,假设申请内存512MB,则每份1MB。也就是每个bucket 1MB内存。
  • 分为512个桶,每个桶再单独初始化

4.2.3 SET方法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

func (c *Cache) Set(k, v []byte) {      h := xxhash.Sum64(k)      idx := h % bucketsCount      c.buckets[idx].Set(k, v, h)}

非常简单:对key计算一个hash值,然后对hash值取模,转到具体的bucket对象里面去处理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

4.3 Bucket类实现

4.3.1 bucket结构文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

type bucket struct {      mu sync.RWMutex
      // chunks is a ring buffer with encoded (k, v) pairs.      // It consists of 64KB chunks.      chunks [][]byte
      // m maps hash(k) to idx of (k, v) pair in chunks.      m map[uint64]uint64
      // idx points to chunks for writing the next (k, v) pair.      idx uint64
      // gen is the generation of chunks.      gen uint64
      getCalls    uint64  // 以下都是用于统计的变量      setCalls    uint64      misses      uint64      collisions  uint64      corruptions uint64}
  • mu sync.RWMutex : 每个bucket有一个读写锁来处理并发。
  1. 和sync.Map比起来,原理上也没什么神秘的。把数据分散到512个桶,相当于竞争变为原来的1/512。
  • chunks [][]byte: 这个是存储数据的chunk的数组
  1. 假设每个bucket 1MB, 则共有1MB/64KB=16个chunk
  2. 第15个chunk满了以后,又回到第0个chunk存储,同时gen字段增加,说明是新的一代
  3. chunk是上面提到的通过mmap分配的64KB的一个块
  4. key+value的数据会被顺序的放在chunk中,并记录位于数组中的下标
  5. 一个chunk的空间用完后,会再通过getChunk()再申请64KB的块。直到块达到用户规定的上限。
  • m map[uint64]uint64: 这里存储每个hashcode对应的chunk中的偏移量。
  • idx uint64: 这里记录下次插入chunk的位置,插入完成后跳转到数据的末位。
  • gen uint64: 当所有的chunks都写满以后,gen的值加1,从第0块开始淘汰旧数据。
  • 这里有个明显的缺点:假设hashcode都分布在较少的几个bucket中,那么就导致某几个bucket的数据频繁淘汰,而其他的bucket还剩挺多空间。不过,这只是假设,并未有数据证明会有这种现象。

4.3.2 Set过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

源码比较多,大致如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  • 每set 16384(2的14次方)次,执行一次clean操作
  1. clean操作遍历整个map,移除chunk中因为回绕淘汰的数据
  • key+value序列化的方式很简单,顺序存储以下内容:
  1. 2字节key长度
  2. 2字节value长度
  3. key的内容
  4. value的内容
  • 写入chunk的时候加入了写锁
  • 通过bucket的idx字段找到插入位置,然后按照上述序列化的方式拷贝数据
  • 插入完成后得到了偏移位置,把key的hashcode作为键,把chunks中的偏移量为值,写入字段m的map中
  1. value这里还有个细节:value是64位的uint64, value的低40位存储偏移量,value的高24位存储generation的信息。

4.3.3 Get过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  • 首先在Cache类中,根据key的hashcode,确定选择哪个bucket
  • 查询前加读锁
  • 在m字段的map中,根据hashcode找到下标
  • 根据下标确定key的位置
  • 比较key的内容是否相等
  • 最后返回value

4.3.4 Del过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

del仅删除map中的key,而chunks中对应的位置只能等到下次回绕才能清理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/49361.html

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/bc/49361.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定