首页/数据/数据库速查/请求合并 / 单飞(singleflight)
Redis 运维与场景124 条

请求合并 / 单飞(singleflight)

同一键的并发回源合并为一个:进程内 singleflight 或分布式锁版本;是击穿与穿透场景之外的通用减负手段。

数据库速查数据更新于 2026-10-13

请求合并(singleflight)是什么

同一时刻对同一个键的回源请求合并成一次:第一个请求去查数据库并回填缓存,其余并发请求等待同一个结果。进程内实现通常用 singleflight 或异步合并队列;跨进程则需要分布式锁加双检,并用短超时兜底。它是缓存击穿治理与通用减负手段——热点键失效瞬间,若不做合并,成百上千个请求会同时打到数据库。

基本信息

适用场景热点键失效瞬间的并发回源(缓存击穿)
进程内方案singleflight(Go)、异步合并队列、锁 + 双检
跨进程方案分布式锁(Redis SET NX)+ 双检 + 短超时
合并窗口毫秒级,窗口过长会放大单次失败的传播范围
兜底策略超时降级返回旧值或默认值,避免全部请求排队
配套手段逻辑过期(后台异步刷新)避免集中失效

典型使用场景

  • 热点商品详情:缓存过期瞬间的并发回源合并
  • 配置与字典数据:读多写少且被高频读取
  • 启动预热:大量实例同时启动时避免集体打库
  • 缓存穿透防护的补充:空值缓存之外再加合并
  • 慢查询保护:回源本身很慢时,合并能显著降低数据库压力

Go singleflight 示例

Go singleflight 示例
import "golang.org/x/sync/singleflight"

var g singleflight.Group

func GetUser(uid string) (string, error) {
    v, err, _ := g.Do("user:"+uid, func() (interface{}, error) {
        if v := redis.Get("user:" + uid); v != nil {
            return v, nil          // 命中缓存直接返回
        }
        row, err := db.Query(uid)  // 并发只执行一次回源
        if err == nil {
            redis.Set("user:"+uid, row, 300)
        }
        return row, err
    })
    return v.(string), err
}

方案对照

方案适用范围代价
singleflight单进程内的并发合并无法跨进程合并,实例多时效果有限
分布式锁 + 双检跨进程合并锁本身的可用性成为关键路径
逻辑过期热点键不集中失效需要后台刷新任务与数据一致性权衡
空值缓存防穿透(查询不存在的数据)需控制空值 TTL,避免掩盖数据新增
合并窗口要短(毫秒级):窗口太长会把单次回源的失败传播给所有等待者,也会让整体延迟被最慢的那次回源拖住;配合超时与降级(返回旧值或默认值)才是完整方案。
合并键必须包含全部查询参数:只按用户 ID 合并而忽略其他筛选条件,会把不同请求的数据相互串用,这类错误在压测中很难发现,往往上线后由用户投诉才暴露。

易错点

  • 合并 key 设计错误:不同用户的请求被合并,返回他人数据
  • 没有超时控制:一个慢回源拖住所有合并请求,整体雪崩
  • 用 distributed lock 但不做双检:拿到锁后未重查缓存,重复回源
  • 锁未设置超时或未可靠释放:锁泄漏导致该键长时间无法刷新
  • 只做合并不做空值缓存:对不存在的数据反复回源,数据库压力依旧

数据来源与参考

  • Redis 官方文档: 缓存模式
  • singleflight 库文档

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌