groupcache 工作原理

一. 简介

groupcache 是一个内嵌在 Go 应用中的分布式缓存库,它通过节点间的自动协作来共享缓存,并通过独有的 single-flight 机制,从根本上解决了缓存惊群问题,非常适合为数据库或 API 等后端服务提供一个健壮、高效的只读缓存层。

二. 核心组件

  1. Group: 这是我们打交道的顶层对象。它像一个司令部,管理着一个特定缓存命名空间的所有资源和操作。它包含:

    • name: 缓存组的唯一名称。
    • getter: 用户定义的 Getter 接口,是数据的最终来源。
    • peers: 一个 PeerPicker 接口,负责选择节点。
    • mainCache & hotCache: 一个两级缓存系统。mainCache 是主缓存,采用 LRU 算法存储数据;hotCache 是一个容量很小的"热点缓存",也用 LRU 存储,用于存放近期访问最频繁的少量数据,以避免对 mainCache 的并发锁竞争,性能更高。
    • loader: 一个 singleflight.Group 实例,这是防止缓存惊群的关键。
  2. PeerPicker (通常是 HTTPPool): 这是通信系统。它维护着集群中所有节点(Peers)的列表,并能根据 key 挑选出应该负责这个 key 的节点。HTTPPool 是它的标准实现,负责通过 HTTP 进行节点间通信。

  3. consistenthash.Map: 这是决策系统或"导航员"。PeerPicker 内部就用它来管理一致性哈希环。它能根据给定的 key,快速、确定地计算出这个 key 应该由哪个节点来管理。

  4. 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 首先会尝试从本地获取数据,这是最快的路径:

  1. 检查 hotCache(热点缓存)。如果命中,直接返回数据,流程结束。
  2. 如果 hotCache 未命中,则检查 mainCache(主缓存)。如果命中,将数据提升到 hotCache 中,然后返回数据,流程结束。

第 3 步:进入 singleflight 门卫

如果本地缓存全部未命中,请求不会立即冲向网络。它会先经过 loader (singleflight.Group) 的处理。

  • loader 会检查当前有没有其他 goroutine 正在处理 key="apple"。
  • 如果是第一个:允许通过,继续执行后续步骤。
  • 如果不是第一个:在此阻塞等待,直到第一个 goroutine 完成并返回结果。所有等待者都会共享这个结果,然后直接返回,流程结束。

第 4 步:定位主节点 (Peer Picking)

通过 singleflight 的那个幸运的 goroutine 现在需要决定谁来加载数据。

  1. 它会调用 group.peers.PickPeer("apple")。
  2. 内部的 consistenthash.Map 开始计算,确定 key="apple" 的主节点是节点 C。

第 5 步:决策:本地加载还是远程获取?

PickPeer 会返回一个代表节点 C 的 ProtoGetter 接口。

  1. groupcache 检查这个 ProtoGetter 是不是代表自己(节点 A)。
  2. 发现不是,因此确定这是一次远程获取(Remote Fetch)。
  3. 它会调用这个 ProtoGetter 的 Get 方法。在 HTTPPool 的实现中,这会向节点 C 发起一个 HTTP 请求,例如:GET http://address-of-C/groupcache/music/apple
  4. 节点 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)

  1. 节点 C 的 Get 方法开始执行,同样先检查自己的 hotCache 和 mainCache。假设它也没有缓存 apple。
  2. 请求进入节点 C 的 singleflight 门卫,并成为第一个通过的。
  3. 节点 C 调用 PickPeer("apple"),这次计算结果返回的是它自己。
  4. groupcache 发现主节点就是自己,于是确定这是一次本地加载(Local Load)。
  5. 它调用用户定义的 Getter 函数 (getFromDB("apple"))。
  6. Getter 函数执行,从慢速数据库中获取到 "red" 这个值。
  7. 获取成功后,将 "red" 填充到节点 C 的 mainCache 中。
  8. 将 "red" 这个结果写入 HTTP 响应,并发送回给节点 A。

回到发起者节点 A

第 9 步:接收并缓存结果

  1. 节点 A 收到了来自节点 C 的 HTTP 响应,其中包含了数据 "red"。
  2. 节点 A 会将这个从远程获取到的数据同样存入自己的 mainCache 中。这很重要,这意味着发起请求的节点也会缓存数据,以备下次自己使用。
  3. singleflight 将这个结果广播给所有在第 3 步等待的、针对 key="apple" 的 goroutine。

第 10 步:返回最终结果

数据被写入 dest,Get 方法调用返回。应用程序在节点 A 上成功获取到了 "red"。

流程图

相关推荐
千叶寻-1 小时前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
小咕聊编程2 小时前
【含文档+源码】基于SpringBoot的过滤协同算法之网上服装商城设计与实现
java·spring boot·后端
追逐时光者8 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_8 小时前
敏捷开发流程-精简版
前端·后端
苏打水com9 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧10 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧10 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧10 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧10 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧10 小时前
Spring Cloud Gateway详解与应用实战
后端