如何在Golang中实现接口缓存_Web接口缓存实现思路

缓存逻辑必须置于handler外层作为中间件,统一处理key生成、读写及TTL控制;禁用仅缓存成功响应、忽略状态码、滥用sync.Map等错误做法。

缓存逻辑必须放在 handler 外层,不能塞进业务函数里

Go 的 HTTP handler 本身是无状态的,但缓存需要跨请求共享数据。如果把 cache.Get / cache.Set 写在业务逻辑函数内部(比如 getUserByID),会导致缓存无法复用——因为每次调用都走新上下文,且没做 key 统一或过期控制。

正确做法是把缓存作为中间件或 wrapper 套在 handler 上,统一拦截请求、生成 key、读写缓存。常见错误是:只缓存成功响应,却忽略 404 或 500 状态码的缓存策略,结果反复穿透到后端。

  • key 必须包含 method + path + query string(用 req.URL.String() 安全但注意 URL 解码差异)
  • 对 POST/PUT 请求,若要缓存,需额外序列化 req.Body 并参与 key 计算(通常不推荐)
  • 缓存 value 应该是完整 *http.Response 的序列化结果(含 status code、headers、body),而非仅 body 字节

sync.Map 做本地缓存时要注意 GC 和内存泄漏

sync.Map 适合读多写少、key 数量可控的场景,但它是无过期机制的。直接用它当接口缓存,很容易积累大量 stale 数据,尤其当请求带动态参数(如 /user?id=123/user?id=456)时,key 不可复用,Map 持续膨胀。

更稳妥的做法是封装一层带 TTL 的本地缓存,比如用 time.Now().UnixNano() 存入 value,并在 Get 时比对。别依赖 sync.Map 自动清理——它不会删过期项。

type CacheItem struct {
	Data    []byte
	Expires int64 // Unix nanos
}
func (c *LocalCache) Get(key string) ([]byte, bool) {
	if v, ok := c.m.Load(key); ok {
		item := v.(CacheItem)
		if time.Now().UnixNano() < item.Expires {
			return item.Data, true
		}
		c.m.Delete(key) // 主动清理
	}
	return nil, false
}

用 Redis 实现分布式缓存时,key 设计要防哈希冲突

多个服务实例共用一个 Redis,key 必须全局唯一且可预测。常见错误是直接用 req.RequestURI 当 key,但不同客户端可能发送带空格、未编码的 URL,Redis 中会视为不同 key;或者忽略 header 差异(如 Accept: application/json vs application/xml),导致缓存错乱。

建议标准化 key 生成逻辑:

  • 对 path 和 query 使用 url.PathEscapeurl.QueryEscape
  • 若需区分 content-type,把 req.Header.Get("Accept") 哈希后截取前 8 位拼入 key
  • 加固定前缀如 "api:v2:",便于 redis-cli 批量清理
  • 避免在 key 中放用户 ID 等敏感字段(除非已脱敏或加密)

缓存失效不是“删掉就完事”,得考虑并发击穿

当某个热点接口缓存过期瞬间,大量请求同时穿透到后端,可能打挂 DB 或下游服务。单纯用 DEL key 触发失效,没有保护机制。

可行方案有二:

  • 使用「逻辑过期」:value 里存一

    expire_at 字段,缓存未物理删除,get 时发现逻辑过期则触发异步回源更新(用 redis.SetNX 抢锁)
  • 预热 + 延迟双删:在定时任务中提前 30s 刷新即将过期的 key;更新 DB 后,先删缓存 → 写 DB → 延迟几百毫秒再删一次缓存

无论哪种,都要监控 cache.miss_ratebackend.qps,否则失效策略是否生效根本没法验证。