如何避免或延迟缓存缺失时的额外工作?如何缓解缓存缺失避免频繁调用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
相关产品推荐
相关产品推荐

