如何使用Golang实现代理缓存系统_Golang代理模式请求优化示例

直接用 http.Transport 做代理缓存会失败,因其默认不缓存响应,且 RoundTripper 接口无法拦截并缓存响应体,多次读取 resp.Body 会导致“read on closed response body”错误。

为什么直接用 http.Transport 做代理缓存会失败

Go 标准库的 http.Transport 默认不缓存响应,即使你设置了 Cache-Control: public, max-age=3600,它也不会自动复用。更关键的是,http.RoundTripper 接口不暴露原始请求头/响应体,无法在中间拦截并写入本地缓存。常见错误是试图在 RoundTrip 方法里读取 resp.Body 两次——第一次读完后 Body 就被关闭,第二次读会得到空内容或 http: read on closed response body 错误。

  • 必须在读取响应前,用 io.TeeReaderio.MultiReader 复制原始字节流
  • 缓存键不能只依赖 req.URL.String(),要归一化:去掉 utm_* 参数、忽略查询参数顺序、统一协议和 host 大小写
  • Cache-Control 中的 no-storeno-cacheprivate 必须严格跳过缓存

http.ServeHTTP 实现可缓存的反向代理

标准库 httputil.NewSingleHostReverseProxy 是起点,但它不支持缓存逻辑注入。你需要继承并重写 Director 和响应处理流程。核心是在 proxy.ServeHTTP 调用后,立即拦截 ResponseWriter,捕获状态码、头信息和响应体。

  • httptest.NewRecorder() 拦截响应,再用 bytes.Buffer 存储原始 body
  • 缓存键生成示例:fmt.Sprintf("%s %s %s", req.Method, normalizeURL(req.URL), req.Header.Get("Accept"))
  • 缓存过期时间优先取 Cache-Control: max-age=N,其次 fallback 到 Expires
func (c *cachedProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    key := c.cacheKey(req)
    if entry, ok := c.cache.Get(key); ok && !entry.Expired() {
        entry.WriteTo(rw)
        return
    }

    resp := httptest.NewRecorder()
    c.proxy.ServeHTTP(resp, req)

    buf := &bytes.Buffer{}
    io.Copy(buf, resp.Body)
    bodyBytes := buf.Bytes()

    cacheEntry := &cacheItem{
        StatusCode: resp.Code,
        Header:     resp.Header().Clone(),
        Body:       bodyBytes,
        CreatedAt:  time.Now(),
        TTL:        c.ttlFromHeaders(resp.Header()),
    }
    c.cache.Set(key, cacheEntry, cacheEntry.TTL)

    rw.WriteHeader(resp.Code)
    for k, vs := range resp.Header() {
        for _, v := range vs {
            rw.Header().Add(k, v)
        }
    }
    rw.Write(bodyBytes)
}

bigcacheristretto 选哪个做内存缓存

bigcache 适合高并发、大量小响应(如 JSON API),因为它用分片 map + 时间戳淘汰,避免 GC 压力;但不支持基于 TTL 的自动过期,得自己轮询清理。ristretto 支持精确 TTL、LRU+LFU 混合策略,更适合混合大小响应(含图片、HTML),但内存占用略高、初始化稍慢。

  • 如果代理目标主要是 REST API(平均响应 bigcache.NewBigCache,设置 Shards: 128
  • 如果需缓存 HTML 或压缩资源(可能 >100KB),用 ristretto.NewCache,开启 OnExit: func(key uint64, value interface{}) 清理大对象
  • 避免用 sync.Map:它不支持容量限制和淘汰,长期运行必然 OOM

如何处理 POST/PUT 请求的缓存穿透

GET 请求天然幂等,可安全缓存;但 POST/PUT 默认不可缓存。若业务明确某些 POST 是只读操作(如 /api/search),需手动放行。关键是识别“语义 GET”:检查路径是否含 searchquerylist,且请求体是 J

SON 查询条件而非业务变更数据。

  • 禁止缓存 Content-Type: application/json 且 body 含 "update""delete""status": "active" 等字段的请求
  • 对放行的 POST,缓存键必须包含 sha256(body),而非仅 URL —— 否则不同查询参数会命中同一缓存
  • 永远不缓存带 CookieAuthorization 头的请求,除非显式配置 CacheableHeaders: ["X-User-ID"]

缓存系统最难的不是存和取,而是判断“该不该缓存”——这个决策点分散在 URL 归一化、头解析、body 检查、TTL 计算多个环节,漏掉任意一个都可能把私有数据缓存成公共响应。