当前位置:首页 > 文章列表 > Golang > Go教程 > Go singleflight 实战:别让缓存击穿打爆下游服务

Go singleflight 实战:别让缓存击穿打爆下游服务

来源:Go 官方扩展包文档 2026-06-04 14:03:23 0浏览 收藏

缓存击穿这事,我见过最典型的现场是这样的:一个热门商品刚好过期,几百个请求同时打进来,全都发现缓存没命中,然后一起回源查数据库或者调用下游接口。业务日志看起来只是“缓存 miss 多了一点”,但下游 QPS 会突然尖起来,慢一点就是连锁超时。

Go 里处理这种“同一个 key 的重复请求”有一个很顺手的工具:golang.org/x/sync/singleflight。它不是缓存,也不是限流器,它做的事情很窄:把同一个 key 上同时发生的重复函数调用合并起来,让真正回源的调用只执行一次,其他请求等结果并共享。

Go singleflight 请求合并思维导图
思维导图:singleflight 的核心不是缓存,而是同一时刻的重复调用抑制。

先把边界说清楚:singleflight 不是万能缓存

我最怕团队把 singleflight 当成“高级缓存”。它本身不存业务数据,也不会替你设置 TTL,更不会判断数据新不新。它只管一件事:当前这一小段时间里,某个 key 的回源函数是不是已经有人在跑了。如果有人在跑,后来的请求就等这个结果。

官方包里的 Group.Do 会按 key 执行函数,返回值里有一个 shared,表示结果是否被多个调用者共享。这个字段很适合打指标:如果 shared 比例突然升高,通常说明热点 key、缓存失效或者下游变慢正在出现。

事故复现:缓存 miss 后每个请求都回源

先看一个很常见的写法。代码没有语法问题,也能上线跑很久,但在热门 key 失效时,它会把并发请求原封不动地打到下游。

func (s *Service) GetProduct(ctx context.Context, id string) (*Product, error) {
    key := "product:" + id
    if v, ok := s.cache.Get(key); ok {
        return v.(*Product), nil
    }

    // 热点 key 过期时,每个请求都会走到这里
    p, err := s.repo.LoadProduct(ctx, id)
    if err != nil {
        return nil, err
    }
    s.cache.Set(key, p, 30*time.Second)
    return p, nil
}

这个问题低峰不明显,因为并发不够大;压测如果没有故意打同一个 key,也很容易漏掉。真正上生产后,爆点通常发生在首页推荐、商品详情、权限配置、汇率价格、风控规则这类热点数据上。

singleflight 缓存击穿治理流程图
流程图:缓存 miss 之后,不要让所有请求一起回源,同一个 key 先进入 singleflight 合并。

落地写法:缓存 miss 后再进入 singleflight

我一般不会把 singleflight 放在最外层。正确顺序是:先查缓存,命中直接返回;未命中,再用 singleflight 合并同一个 key 的回源动作;回源成功后写缓存,等待方共享结果。

type Service struct {
    cache Cache
    repo  ProductRepo
    group singleflight.Group
}

func (s *Service) GetProduct(ctx context.Context, id string) (*Product, error) {
    key := "product:" + id
    if v, ok := s.cache.Get(key); ok {
        return v.(*Product), nil
    }

    v, err, shared := s.group.Do(key, func() (any, error) {
        if v, ok := s.cache.Get(key); ok {
            return v.(*Product), nil
        }

        p, err := s.repo.LoadProduct(ctx, id)
        if err != nil {
            return nil, err
        }
        s.cache.Set(key, p, 30*time.Second)
        return p, nil
    })
    if err != nil {
        return nil, fmt.Errorf("load product %s: %w", id, err)
    }

    observeSingleflight("product", shared)
    return v.(*Product), nil
}

注意函数内部我又查了一次缓存,这不是多余。因为在当前 goroutine 等待排队期间,可能已经有别的请求把缓存写回去了。这个二次检查能避免一些无意义的回源,尤其是你在外层还有本地缓存、二级缓存的时候。

Key 粒度别拍脑袋

singleflight 最容易写错的是 key。key 太粗,会把不该合并的请求合到一起,比如不同租户、不同语言、不同权限范围被合并,结果就有串数据风险。key 太细,又合并不了请求,等于只加了复杂度。

我的习惯是把影响结果的维度全部写进 key:业务对象 ID、租户、地区、语言、灰度版本、权限维度。只要这些维度里有一个会改变结果,就不能省。这个原则比“key 短一点好看”重要得多。

Go singleflight 缓存击穿修复前后案例图
案例图:修复前每个请求都回源,修复后同一个 key 只让一个请求真正打下游。

错误传播:别把失败放大,也别把错误缓存太久

singleflight 会把同一次执行的错误也共享给等待者。这是合理的,因为它们等的是同一个回源动作。但这也意味着,一次下游失败可能会让一批请求同时拿到错误。这里不要误会:singleflight 减少的是重复回源,不保证下游一定成功。

如果回源失败,我通常不会把错误长期缓存。最多做非常短的负缓存,而且要看业务能不能接受。比如商品详情失败,可能宁愿返回错误;权限配置失败,可能要降级到上一次可用版本;风控规则失败,就不能随便兜底。

Forget 什么时候用

Forget(key) 的意思是让 Group 忘掉某个 key,后续请求不再等当前这次调用。它不是日常必备动作,但在一些场景很有用:比如当前回源已经确定卡死、业务决定快速失败并允许下一批请求重新尝试,或者你做了更高层的熔断和降级。

别把 Forget 当成“修复错误”的按钮。乱用 Forget 会让请求重新并发回源,反而把 singleflight 的保护效果打没。我的建议是:只有当你明确知道当前这次调用不应该再被等待时,才使用它。

DoChan:异步等待也要有超时

如果你想用 select 同时等结果和 context 取消,可以用 DoChan。它会返回一个 channel,里面是结果、错误和 shared 标记。但这里有个坑:调用方取消等待,不代表回源函数自动停掉。回源函数自己也必须尊重 context。

ch := s.group.DoChan(key, func() (any, error) {
    return s.repo.LoadProduct(ctx, id)
})

select {
case ret := <-ch:
    if ret.Err != nil {
        return nil, ret.Err
    }
    return ret.Val.(*Product), nil
case <-ctx.Done():
    return nil, ctx.Err()
}

如果回源函数内部又自己创建了 context.Background(),那外层超时就失效了。这个问题我在代码 review 里会特别盯,因为它很隐蔽:表面上用了 DoChan 和 select,实际回源根本不听取消。

上线前我会加哪些指标

  • shared 比例:shared=true 越高,说明请求合并越频繁,热点或缓存失效越明显。
  • 回源耗时:只看接口耗时不够,要单独看回源函数耗时。
  • 每个 key 的合并量:必要时采样记录热点 key,避免全量打爆日志。
  • 错误类型:区分下游超时、业务不存在、序列化失败和 context 取消。
  • 缓存命中率:singleflight 是止血,不是替代缓存命中率治理。
  • 下游 QPS:上线前后对比同一热点场景下的回源次数。

我自己的使用边界

singleflight 适合“同一时刻、同一个 key、重复回源成本很高”的场景。它不适合替代队列,不适合做全局限流,也不适合解决所有慢查询。你要先确认问题是重复调用,而不是单次调用本身太慢。

如果下游本身已经慢到不可接受,singleflight 只能减少并发回源,不能让慢调用变快。这个时候还要配合超时、熔断、降级、缓存预热和容量治理。工具要放在正确的位置,才不会变成新的复杂度。

最后聊两句

我喜欢 singleflight 的原因是它足够小:不抢缓存的活,不抢限流的活,只负责把重复回源合并掉。也正因为它小,使用时更要把边界写清楚:key 怎么拼、错误怎么处理、超时怎么传、shared 怎么观测。

如果你的 Go 服务里有热点缓存、配置加载、权限查询、商品详情、价格汇率这类场景,建议专门做一次同 key 并发压测。你会很快看出来,singleflight 到底是在帮你挡缓存击穿,还是只是给代码加了一层看起来很高级的包装。

版本声明
本文转载于:Go 官方扩展包文档 如有侵犯,请联系study_golang@163.com删除
Spring Boot 集成测试别再只靠 H2:Testcontainers 落地踩坑复盘Spring Boot 集成测试别再只靠 H2:Testcontainers 落地踩坑复盘
上一篇
Spring Boot 集成测试别再只靠 H2:Testcontainers 落地踩坑复盘
G1 GC 暂停飙升排查:别先复制 JVM 参数,先看 JFR 和 GC 日志
下一篇
G1 GC 暂停飙升排查:别先复制 JVM 参数,先看 JFR 和 GC 日志
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    2137次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    1980次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    1925次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    2129次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    2109次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码