You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免或延迟缓存缺失时的额外工作?如何缓解缓存缺失避免频繁调用API?

缓解持续缓存缺失的推荐模式

通用场景方案

  • 缓存缺失标记(空值缓存):把确认不存在的key也存入缓存,用特殊值(比如nil或自定义空结构体)标记,同时设置合理过期时间(比如5-15分钟)。后续请求先查缓存,若命中这个标记,直接返回“不存在”,无需调用API。过期时间可根据业务数据新增可能性调整——如果数据几乎不会新增,可设更长时间。
  • 指数退避机制:如果必须重试不存在的key,不要立即发起请求。第一次等1秒,第二次2秒,第三次4秒,以此类推,直到达到最大间隔(比如30秒)后保持固定间隔。避免短时间内重复请求打爆API端点。
  • 请求合并(防抖):高并发场景下,同一时间段内多个相同key的缺失请求,只发起一次API调用,其他请求等待这次调用的结果。比如用“请求中”标记map,请求过来时若发现该key正在请求,就阻塞等待结果,否则发起请求并标记,完成后清除标记。
  • 永久黑名单:对于确认永久不存在的key(比如业务明确不会新增的无效key),加入永久或超长过期的黑名单。后续请求先检查黑名单,命中则直接返回“不存在”,彻底杜绝API请求。

Golang 具体实现示例

1. 空值缓存+指数退避

import (
    "sync"
    "time"
)

type APICache struct {
    cache sync.Map
}

// 自定义缺失标记,避免和正常nil值混淆
var missingFlag = struct{}{}

func (ac *APICache) Get(key string) (interface{}, error) {
    // 优先查缓存
    val, exists := ac.cache.Load(key)
    if exists {
        if val == missingFlag {
            // 命中缺失标记,直接返回不存在
            return nil, nil
        }
        return val, nil
    }

    // 缓存缺失,带指数退避调用API
    var result interface{}
    var err error
    backoff := 1 * time.Second
    maxBackoff := 30 * time.Second
    retryTimes := 3

    for i := 0; i < retryTimes; i++ {
        result, err = callExternalAPI(key)
        if err == nil && result != nil {
            break
        }
        // 等待后重试
        time.Sleep(backoff)
        backoff *= 2
        if backoff > maxBackoff {
            backoff = maxBackoff
        }
    }

    // 处理API返回为空/失败的情况
    if err != nil || result == nil {
        // 存入缺失标记,缓存5分钟
        ac.cache.Store(key, missingFlag)
        go func() {
            time.Sleep(5 * time.Minute)
            ac.cache.Delete(key)
        }()
        return nil, nil
    }

    // 缓存有效数据(假设数据极少变更,设置24小时过期)
    ac.cache.Store(key, result)
    go func() {
        time.Sleep(24 * time.Hour)
        ac.cache.Delete(key)
    }()
    return result, nil
}

// 模拟外部API调用
func callExternalAPI(key string) (interface{}, error) {
    // 实际业务逻辑:调用网络API,返回结果或错误
    return nil, nil
}

2. 请求合并实现思路

import (
    "sync"
    "time"
)

type APICache struct {
    cache sync.Map
    reqMu sync.Mutex
    // 记录正在进行的请求,key为请求的key,value为等待组
    pendingReqs map[string]*sync.WaitGroup
}

func (ac *APICache) Init() {
    ac.pendingReqs = make(map[string]*sync.WaitGroup)
}

func (ac *APICache) Get(key string) (interface{}, error) {
    val, exists := ac.cache.Load(key)
    if exists {
        if val == missingFlag {
            return nil, nil
        }
        return val, nil
    }

    ac.reqMu.Lock()
    // 检查是否已有同key的请求在进行
    wg, exists := ac.pendingReqs[key]
    if exists {
        ac.reqMu.Unlock()
        wg.Wait()
        // 等待完成后再次查缓存
        return ac.Get(key)
    }
    // 创建等待组,标记请求开始
    wg = &sync.WaitGroup{}
    wg.Add(1)
    ac.pendingReqs[key] = wg
    ac.reqMu.Unlock()

    // 执行API调用
    result, err := callExternalAPI(key)
    // 更新缓存
    if err != nil || result == nil {
        ac.cache.Store(key, missingFlag)
        go func() {
            time.Sleep(5 * time.Minute)
            ac.cache.Delete(key)
        }()
    } else {
        ac.cache.Store(key, result)
        go func() {
            time.Sleep(24 * time.Hour)
            ac.cache.Delete(key)
        }()
    }

    // 标记请求完成,释放等待的goroutine
    ac.reqMu.Lock()
    wg.Done()
    delete(ac.pendingReqs, key)
    ac.reqMu.Unlock()

    return result, err
}

注意事项

  • 缺失标记的过期时间要平衡“避免频繁请求”和“不遗漏新增数据”的需求,如果业务允许数据后续新增,过期时间不能设置过长。
  • 退避的重试次数和间隔要根据外部API的限流规则调整,避免触发API的封禁或限流。
  • 请求合并在高并发场景下能大幅减少重复API请求,但要注意goroutine泄漏问题,确保请求完成后及时清理pending标记。

内容的提问来源于stack exchange,提问作者hebeha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 13:35:54