一. 简介
groupcache 是一个内嵌在 Go 应用中的分布式缓存库,它通过节点间的自动协作来共享缓存,并通过独有的 single-flight
机制,从根本上解决了缓存惊群问题,非常适合为数据库或 API 等后端服务提供一个健壮、高效的只读缓存层。
二. 核心组件
-
Group: 这是我们打交道的顶层对象。它像一个司令部,管理着一个特定缓存命名空间的所有资源和操作。它包含:
- name: 缓存组的唯一名称。
- getter: 用户定义的 Getter 接口,是数据的最终来源。
- peers: 一个 PeerPicker 接口,负责选择节点。
- mainCache & hotCache: 一个两级缓存系统。mainCache 是主缓存,采用 LRU 算法存储数据;hotCache 是一个容量很小的"热点缓存",也用 LRU 存储,用于存放近期访问最频繁的少量数据,以避免对 mainCache 的并发锁竞争,性能更高。
- loader: 一个 singleflight.Group 实例,这是防止缓存惊群的关键。
-
PeerPicker (通常是 HTTPPool): 这是通信系统。它维护着集群中所有节点(Peers)的列表,并能根据 key 挑选出应该负责这个 key 的节点。HTTPPool 是它的标准实现,负责通过 HTTP 进行节点间通信。
-
consistenthash.Map: 这是决策系统或"导航员"。PeerPicker 内部就用它来管理一致性哈希环。它能根据给定的 key,快速、确定地计算出这个 key 应该由哪个节点来管理。
-
singleflight.Group: 这是"防拥堵的门卫"。它的作用是,对于一个正在处理的 key,只允许第一个请求(goroutine)通过,其他后来的、针对同一个 key 的请求都会在此等待,直到第一个请求完成。完成后,所有等待者都会获得相同的结果。
三. golang使用
groupcache.NewGroup 创建一个新的缓存组:
golang
func setupGroupCache(cacheSizeBytes int64) {
// 参数:
// 1. name: 组的唯一名称。
// 2. cacheSizeBytes: 分配给该组的总缓存大小(所有节点共享这个逻辑限制)。
// 3. getter: a groupcache.Getter (这是核心!)
musicGroup = groupcache.NewGroup("music", cacheSizeBytes, groupcache.GetterFunc(
// GetterFunc 是一个实现了 Getter 接口的函数类型。
// 当缓存未命中时,groupcache 会调用这个函数。
func(ctx context.Context, key string, dest groupcache.Sink) error {
log.Printf("[GROUPCACHE] Cache miss for key: %s. Looking up in DB...", key)
// 从我们的"慢速数据库"获取数据
value, err := getFromDB(key)
if err != nil {
return err
}
// 将获取到的数据填充到缓存中。
// SetString 方法会自动处理数据的序列化。
dest.SetString(value)
return nil
},
))
}
创建一个 API 接口来使用 groupcache:
golang
func startAPIServer(apiAddr string) {
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if key == "" {
http.Error(w, "Missing key parameter", http.StatusBadRequest)
return
}
var data []byte ctx := context.Background()
// musicGroup.Get 是我们与 groupcache 交互的主要方法。
// 它会处理所有事情:检查本地缓存、从远端节点获取、调用 Getter 回源。
if err := musicGroup.Get(ctx, key, groupcache.AllocatingByteSliceSink(&data)); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain") w.Write(data)
})
log.Printf("Starting API server at: %s", apiAddr)
// 注意:这里在一个新的 http.ServerMux 上启动 API 服务,
// 避免与 groupcache 的 peer 通信服务冲突。
// 在实际项目中,通常会用一个路由器(如 a gorilla/mux)来整合。
mux := http.NewServeMux() mux.HandleFunc("/api", http.DefaultServeMux.HandleFunc)
// 把上面定义的 handler 拿过来
http.ListenAndServe(apiAddr, mux)
}
四. 数据流转过程
假设你启动了三个相同的应用实例(A, B, C),它们会自动组成一个缓存集群。
- 当 A 需要某个数据时,groupcache 会通过一致性哈希算法计算出这个数据应该由哪个节点负责(比如是 C)。
- A 会自动通过网络向 C 请求数据,而不需要你手动管理。
- 这个设计让整个集群的缓存空间是所有节点缓存之和,实现了分布式缓存。
在发起者节点 A 内部
第 1 步:调用 Get 方法
应用程序调用 musicGroup.Get(ctx, "apple", dest)。
第 2 步:检查本地缓存 (Fast Path)
groupcache 首先会尝试从本地获取数据,这是最快的路径:
- 检查 hotCache(热点缓存)。如果命中,直接返回数据,流程结束。
- 如果 hotCache 未命中,则检查 mainCache(主缓存)。如果命中,将数据提升到 hotCache 中,然后返回数据,流程结束。
第 3 步:进入 singleflight 门卫
如果本地缓存全部未命中,请求不会立即冲向网络。它会先经过 loader (singleflight.Group) 的处理。
- loader 会检查当前有没有其他 goroutine 正在处理 key="apple"。
- 如果是第一个:允许通过,继续执行后续步骤。
- 如果不是第一个:在此阻塞等待,直到第一个 goroutine 完成并返回结果。所有等待者都会共享这个结果,然后直接返回,流程结束。
第 4 步:定位主节点 (Peer Picking)
通过 singleflight 的那个幸运的 goroutine 现在需要决定谁来加载数据。
- 它会调用 group.peers.PickPeer("apple")。
- 内部的 consistenthash.Map 开始计算,确定 key="apple" 的主节点是节点 C。
第 5 步:决策:本地加载还是远程获取?
PickPeer 会返回一个代表节点 C 的 ProtoGetter 接口。
- groupcache 检查这个 ProtoGetter 是不是代表自己(节点 A)。
- 发现不是,因此确定这是一次远程获取(Remote Fetch)。
- 它会调用这个 ProtoGetter 的 Get 方法。在 HTTPPool 的实现中,这会向节点 C 发起一个 HTTP 请求,例如:GET http://address-of-C/groupcache/music/apple。
- 节点 A 开始等待节点 C 的 HTTP 响应。
在主节点 C 内部
第 6 步:接收 HTTP 请求
节点 C 上的 HTTPPool 服务器接收到来自节点 A 的 HTTP 请求。
第 7 步:本地处理请求
HTTP 处理函数会解析出 groupName="music" 和 key="apple"。然后,它并不会直接去查自己的缓存盘,而是会调用自己本地的 musicGroup.Get(ctx, "apple", dest) 方法。
为什么要这样做?
这是一个非常优雅的设计!这使得节点 C 也能利用自己的两级缓存和 singleflight 机制。万一此时节点 B 也来请求 apple,节点 C 的 singleflight 也能确保只进行一次回源。
第 8 步:在主节点回源 (Cache Filling)
- 节点 C 的 Get 方法开始执行,同样先检查自己的 hotCache 和 mainCache。假设它也没有缓存 apple。
- 请求进入节点 C 的 singleflight 门卫,并成为第一个通过的。
- 节点 C 调用 PickPeer("apple"),这次计算结果返回的是它自己。
- groupcache 发现主节点就是自己,于是确定这是一次本地加载(Local Load)。
- 它调用用户定义的 Getter 函数 (getFromDB("apple"))。
- Getter 函数执行,从慢速数据库中获取到 "red" 这个值。
- 获取成功后,将 "red" 填充到节点 C 的 mainCache 中。
- 将 "red" 这个结果写入 HTTP 响应,并发送回给节点 A。
回到发起者节点 A
第 9 步:接收并缓存结果
- 节点 A 收到了来自节点 C 的 HTTP 响应,其中包含了数据 "red"。
- 节点 A 会将这个从远程获取到的数据同样存入自己的 mainCache 中。这很重要,这意味着发起请求的节点也会缓存数据,以备下次自己使用。
- singleflight 将这个结果广播给所有在第 3 步等待的、针对 key="apple" 的 goroutine。
第 10 步:返回最终结果
数据被写入 dest,Get 方法调用返回。应用程序在节点 A 上成功获取到了 "red"。
流程图
