应使用 time.ticker 而非 time.timer 实现持续缓存刷新,配合 goroutine、sync.rwmutex 和原子版本号保障并发安全与数据一致性,并引入指数退避机制应对依赖失败,测试时通过注入 ticker channel 提升可靠性。

用 time.Ticker 启动基础刷新循环,别用 time.Timer 单次触发
缓存定期刷新必须持续运行,time.Timer 只触发一次,用完就得手动重置,极易漏掉或重复启动。直接上 time.Ticker 更稳:
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() // 记得释放资源
<p>for {
select {
case 注意:这个循环必须在 goroutine 里跑,否则会阻塞主线程;<code>refreshCache()</code> 内部要处理并发安全(比如用 <code>sync.RWMutex</code> 或原子操作),别让多个刷新协程同时写同一块内存。</p><h3>刷新前加锁 + 版本校验,避免脏写覆盖</h3><p>如果缓存是全局变量(比如 <code>var cache map[string]interface{}</code>),多个刷新周期可能重叠(比如网络延迟导致某次 <code>refreshCache()</code> 耗时超 30 秒),不加控制就会写入过期数据。推荐做法:</p>
- 用
sync.RWMutex保护读写,刷新时用mu.Lock(),读时用mu.RLock() - 给缓存加个版本号字段(如
cacheVersion int64),每次刷新前先atomic.LoadInt64(&cacheVersion),刷新完成后用atomic.StoreInt64(&cacheVersion, newVer)更新 —— 读取方可据此判断是否命中最新版 - 不要在
refreshCache()里直接替换整个 map,而是逐 key 更新或用双 buffer 交换指针,减少读取卡顿
错误时自动退避,别让失败刷崩服务
上游依赖(如数据库、API)临时不可用时,连续失败会浪费资源还拖慢系统。在刷新逻辑里套一层指数退避:
backoff := 1 * time.Second
for err != nil {
time.Sleep(backoff)
backoff = time.Duration(float64(backoff) * 1.5)
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
err = fetchNewData()
}注意:退避只对本次刷新生效,下一轮仍从基础间隔(如 30 秒)重新计时;别把错误日志打太频繁,用 log.Printf("cache refresh failed: %v, retry in %v", err, backoff) 就够。测试时用 time.SetTimer 模拟时间推进,别 sleep 等真实秒数
单元测试里等 30 秒才验证缓存刷新?太慢且不稳定。Go 1.20+ 提供了 time.SetTimer 和 time.SetTicker(需配合 time.Now mock),但更简单的是把 ticker 作为参数传入函数,测试时传入自定义 channel:
func startRefresher(tickerCh // 测试
done := make(chan struct{})
go func() {
startRefresher(time.After(100<em>time.Millisecond), func() { close(done) })
}()
select {
case time.Millisecond):
t.Fatal("timeout")
}</em>关键点:函数要可注入 ticker 源,否则无法解耦测试;别在函数内部硬编码 time.NewTicker。实际部署时,刷新间隔和重试策略得根据缓存数据的变更频率和下游稳定性调优,不是设个固定值就完事。最常被忽略的是刷新期间的读一致性 —— 即便加了锁,也要确认读路径没绕过锁直接访问原始结构。











