概述

这是官方 Github 主页上的项目介绍,和 fasthttp 名字一样以 fast 打头,作者对项目代码的自信程度可见一斑。此外该库的核心代码非常轻量, 笔者本着学习的目的分析下内部的代码实现。

基准测试

官方给出了 fastcache, bigcache, 标准库 map, sync.Map 的基准测试比较结果。
从测试的结果中可以看到:
  • fastcache 在所有操作上都要比 bigcache
  • fastcache只写 + 读写混合 操作比标准库的 map, sync.Map 要快,只读 操作比后者要慢

组件特性

  • 高性能
  • 线程安全
  • 设计为存储大量数据 (没有 GC 开销)
  • 自动删除比较旧数据
  • 使用简单
  • 源代码简单且非常轻量
  • 缓存数据可以保存到文件,也可以从文件中加载

示例

从示例代码可以看到,除了初始化时需要指定缓存的大小,组件提供的 API 就是常规的 “键值对” 语义操作,例如 Get, Set, Del 等。

源码分析

笔者选择的 fastcache 版本为 v1.12.1

哈希算法

fastcache 库内部采用的哈希算法为 XXH64

数据结构

常量

因为 桶的大小桶的数据存储重写次数 两个字段存储在一个变量中 (可以并发原子操作),所以这里使用两个常量来区分这两个字段各自的字节数。

Cache 对象

Cache 用来表示 缓存对象,包含了 2 个字段:
  • 用于存储数据的 bucket 桶,数据结构是数组,固定长度 512
  • 用于表示 GetBig/SetBig (针对大数据读写) 方法调用的统计信息

bucket 桶

bucket 桶负责具体数据的获取和存储,每个桶的数据存放量是根据 缓存的数据总量 平均计算得出的 (例如总量为 2GB, 那么每个桶的数据就是 4MB)。

数据结构图

notion image

方法

创建缓存对象

New 根据容量参数创建一个缓存对象并返回。

桶初始化

桶重置

Set 操作

Set 操作分成两部分完成,首先计算出指定 key 所在的桶,然后将 value 存入对应的桶中。

Get 操作

Get 操作分成两部分完成,首先计算出指定 key 所在的桶,然后从桶中获取对应的 value, 作为可选性,调用方可以将接收 value 的变量作为第一个参数传入 (复用 []byte, 节省内存)。

桶的哈希缓存重建

调用关系图

notion image

高性能设计细节

fastcache 采用类似 bigcache 的设计思路:
  • 缓存 由许多 组成,每个桶都持有一个锁 (分段锁),这样可以提高多核 CPU 的性能,因为多个 CPU 可以同时访问不同的桶
  • 每个桶由一个哈希索引 (数据块的位置) 和 65536 个数据块组成,每个桶的指针数量最多为 桶的存储容量 / 64KB (这里指 bucket.chunks 字段)。 例如,64GB 缓存将包含大约 1M 指针,而类似大小的 map[string][]byte 将包含大约 1B 指针,这将导致巨大的 GC 开销
从设计上来说,和每个桶持有一个大的数据块相比,fastcache 采用的 64KB 的数据块减少了内存碎片和总内存使用量。 此外当从 全局数据块空闲区 获取数据块时,会直接调用 Mmap 分配到堆外内存,减少了总内存使用量,因为 GC 会更频繁地收集未使用的内存,无需调整 GOGC。

使用约束和限制

fastcache 组件库的使用有 4 个约束条件,在技术选型的时候比较重要,不过从下面的 4 点要求来看,实际应用中可以通过设计合理的数据类型来规避这些约束条件。
  • 缓存数据的 keyvalue 数据类型必须是 []byte, 如果是其他类型,必须在存储前转换为 []byte
  • 缓存数据大小超过 64K, 必须调用 SetBig 方法存储
  • 缓存数据没有过期时间,只有当缓存数据的数量溢出时,才会删除比较旧的数据,通用的实践是将过期时间存入数据中
  • 缓存数据采用 环形缓冲区 存储,这意味着数据量过大的情况下,新的数据会重写并覆盖掉旧的数据
此外值得注意的是,Set 方法并没有返回值来表示操作的执行结果,这种设计丢失了方法语义,并且在某些极端情况下造成难以排查的 Bug。

小结

本文主要分析了 fastcache 组件的缓存设计与实现,我们可以从中学习到 3 个非常重要的设计技巧: 采用分段锁降低锁的粒度, 采用指纹 + 哈希索引快速定位数据位置map 使用 [uint64]uint32 作为非指针优化从而避免 GC, 在组件的基础上,也许我们可以进一步优化 (例如将桶的锁粒度细化到数据块)?感兴趣的读者可以通过修改源代码进行测试 (毕竟 fastcache 的源代码非常轻量)。 此外需要注意的是,fastcache 提供的 Get, Set 方法参数类型都是 []byte, 这意味着在调用方法前,必须现将具体的数据类型或对象转换为 []byte, 这会带来额外的 CPU 消耗。 最后,fastcache 提供的 缓存数据 <=> 文件 写入和加载功能以及 全局数据块空闲区,由于时间关系,本文不再分析其具体代码实现。

Reference

扩展阅读

notion image

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。