应使用 ristretto 替代手写 LRU 和 sync.Map 做缓存,因其支持 TTL、ARC 淘汰、并发安全及字节级成本控制;需正确配置 NumCounters、MaxCost 与 Cost 回调,并严格校验缓存键与响应体捕获逻辑。

用 ristretto 替代手写 LRU,别碰 sync.Map 做缓存
Go 生态里没有 Caffeine 那种带 TTL + 智能淘汰的本地缓存库,sync.Map 本身不支持过期、不支持成本控制、无法自动驱逐冷数据,硬上等于裸奔。线上 Gin 服务该用 github.com/dgraph-io/ristretto——它支持 ARC 淘汰、并发安全、可配 TTL,且能按字节成本限内存。
关键配置不能漏:
-
NumCounters设为预估 key 总数的 10 倍(比如最多缓 10 万条用户数据,填1_000_000) -
MaxCost是总字节上限,不是条目数;必须配Cost回调函数返回每条 entry 的len(body),否则MaxCost形同虚设 - 别信默认
MaxCost: 100000这种写法——没Cost函数,缓存会无限吃内存
缓存键必须包含 method + path + query string,但跳过敏感字段
GET 请求的缓存键拼成 "GET:/api/user?id=123®ion=cn" 是安全的,但以下情况必须排除:
- 请求头含
Authorization或Cookie:这类响应绑定用户身份,不可复用 - URL 含临时 token、签名参数(如
ts=、sign=):每次请求都不同,缓存命中率为 0 - POST/PUT 请求体含动态内容:除非业务明确幂等且可缓存,否则一律不进本地缓存
建议在中间件开头就做白名单校验:if c.Request.Method != "GET" || hasSensitiveHeader(c) { c.Next(); return }
用自定义 ResponseWriter 捕获响应体,别直接读 c.Writer.Body()
Gin 的 c.Writer 是接口,底层实现不保证支持 Body() 方法,直接调用会 panic。正确做法是包装一层,把响应先写进 *bytes.Buffer:
type recordingWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *recordingWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func (w *recordingWriter) WriteHeader(code int) {
w.ResponseWriter.WriteHeader(code)
}
中间件中替换 writer:
- 执行
c.Writer = &recordingWriter{ResponseWriter: c.Writer, body: &bytes.Buffer{}} - 调
c.Next()让 handler 写入缓冲区 - 从
w.body.Bytes()提取完整响应体,连同状态码、Content-Type 一起序列化后存入ristretto
命中缓存时用 c.Data() 直接写出,绕过后续中间件
缓存命中的响应不能走原 handler 流程,否则可能触发重复鉴权、日志、统计等副作用。正确路径是:
- 在中间件开头检查缓存:
if val, ok := cache.Get(key); ok { ... } - 解包出 status、header map、body bytes 后,调
c.Status(status)和c.Header()设置头 - 最后用
c.Data(status, contentType, body)一次性写出,然后c.Abort()终止链路 - 注意:不要用
c.String()或c.JSON(),它们会再次序列化、覆盖已设 header
真正容易被忽略的是 header 复制逻辑——比如 Content-Type、Cache-Control、X-Cache: HIT 这类标记必须显式设置,否则前端或 CDN 可能误判缓存策略。


















