后台过期清理应封装为带start/stop的结构体,用time.ticker+done chan控制启停,启动于server监听后、停止于优雅关闭前;扫描需分批(50–200 key/次)、基于单次now快照判断过期,并用sync.rwmutex保护读多写少场景。

直接起一个后台 goroutine 扫描 map 做过期清理,90% 的场景下是错的——它会拖慢读性能、引发 GC 压力、漏删或误删,且测试难、停不掉。
用 time.Ticker 启动 goroutine 但别在 init 或 main 里硬写
全局启动清理器会导致服务还没 listen 就开始扫空 map,或者测试时无法关闭 goroutine,造成 panic 或资源泄漏。
- 必须封装成结构体,带
Start()和Stop()方法,内部用done chan struct{}控制退出 - 启动时机放在 HTTP server
ListenAndServe成功之后,或 gRPC serverGracefulStop前调用Stop() - 别设
time.Millisecond级别间隔,缓存条目超 10 万时,time.Second是安全下限
每次只扫固定数量,别 range map
for k, v := range cacheMap 在遍历时删 key 会触发迭代器失效;更糟的是,它会一次性把全部 key 加载进内存,GC 压力陡增。
- 改用手动步进:先
keys := make([]string, 0, 100),再for k := range cacheMap收集一批 key(上限控制在 50–200) - 每次只处理这批 key,避免单次操作耗时过长(>10ms)导致 STW 式卡顿
- 扫描前加
if len(cacheMap) == 0 { continue },空 map 直接跳过
过期判断必须用统一时间快照,别反复调 time.Now()
同一轮清理中,不同 key 的 time.Since(item.ExpiredAt) 可能因系统时间跳变或纳秒级误差,导致部分该删的没删、不该删的被删了。
- 清理函数开头就调一次
now := time.Now(),后续所有判断都基于它 - 存储时用绝对时间
expireAt time.Time,而非相对 TTL,避免重复计算 - 判断写成
if now.After(item.ExpireAt)或if now.Sub(item.ExpireAt) > 0,语义清晰且稳定
读多写少场景必须用 sync.RWMutex,不是 sync.Mutex
清理是低频写操作,但读请求可能每秒上万。用 sync.Mutex 会让所有读阻塞在清理锁上;而 sync.RWMutex 允许多个读并发,只在清理时阻塞新读和写。
- 所有读路径走
R Lock()/RUnlock(),清理函数用Lock()/Unlock() - 切记:不能在持有
R Lock()期间做任何可能阻塞的操作(比如 HTTP 调用、DB 查询),否则整个读通路会被拖慢 - 如果缓存结构已用
sync.Map,它不支持安全遍历+删除组合,得换回map + sync.RWMutex
真正难的不是“怎么删”,而是“删多少”和“何时删”——高频低量比低频大量更可控,但粒度太小又浪费调度开销;快照时间精度影响漏删率,而锁策略选错会直接压垮读吞吐。这些细节不写进代码注释里,下次维护的人大概率会修坏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











