缓存中间件必须在路由注册前通过e.use()调用,否则不会触发;其需置于最外层以实现请求命中判断与响应缓存,且仅处理get/head请求,避免同步io阻塞goroutine。

缓存中间件必须在路由注册前调用 e.Use()
中间件执行顺序直接影响缓存是否生效:如果把缓存中间件写在 e.GET() 之后,它根本不会被触发。Echo 的中间件是洋葱模型,请求进来时按 Use() 注册顺序正向执行,响应返回时逆向执行。缓存逻辑只关心「进来的请求是否命中」和「是否需要缓存响应」,所以必须放在最外层。
常见错误是把缓存逻辑写成路由处理器的一部分(比如在 e.GET("/api/data", cacheMiddleware(handler)) 里包一层),这会导致每个路由单独维护缓存逻辑,无法复用、难以统一过期策略,也绕过了中间件的生命周期控制。
- 正确做法:统一用
e.Use(cacheMiddleware),再注册所有路由 - 缓存中间件应只处理 GET/HEAD 请求,跳过 POST/PUT/DELETE 等非幂等方法
- 需检查
c.Request().URL.Path和查询参数(c.Request().URL.RawQuery)拼成完整缓存 key,否则/users?id=1和/users?id=2会冲突
响应体捕获不能靠 c.Response().Writer 直接写入
Echo 的 Response().Writer 是一个包装过的 http.ResponseWriter,但默认不支持重放(replay)。想缓存响应内容,必须在真正写入前拦截字节流——直接调用 Write() 或 WriteHeader() 会绕过缓存逻辑,导致缓存为空或响应错乱。
标准解法是用自定义 ResponseWriter 包装原 writer,在 Write() 时同时写入内存 buffer 和底层 writer,并在 handler 执行完后决定是否存入缓存。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 不要用
c.Response().Writer.Write([]byte{...})做缓存判断 - 推荐用
echo.NewHTTPError()或c.String()等高层方法后,再通过包装 writer 拦截输出 - 注意 Content-Type:缓存 JSON 接口时,若响应体含中文而 header 缺少
charset=utf-8,Redis 里存的可能是乱码 bytes
缓存键生成要排除动态头和敏感参数
同一个接口,不同用户带不同 Authorization 或 X-User-ID 头时,缓存内容很可能不同。如果 key 无差别拼接 path + query,就会出现 A 用户看到 B 用户的缓存数据。
更隐蔽的问题是时间类参数:?t=1726906500 这种毫秒级时间戳几乎每次都不一样,直接参与 key 会导致缓存完全失效。
- 默认只纳入
Path和白名单查询参数(如page,limit,sort) - 显式忽略
token,sign,t,timestamp等动态参数 - 对 header 做选择性哈希:如需多租户隔离,可取
X-Tenant-ID加入 key;但绝不能包含Cookie或Authorization全值
本地内存缓存 vs Redis:别在中间件里做阻塞 IO
用 redis.Client.Get() 同步调用查缓存,会卡住整个 goroutine。Echo 默认并发模型是每请求一个 goroutine,一旦中间件里有同步网络调用,QPS 会断崖下跌。
简单接口用 sync.Map 或 github.com/patrickmn/go-cache 做本地缓存足够,TTL 控制在秒级;跨实例共享或需精确过期时才上 Redis,且必须用异步方式(如 redis.Client.Get(ctx) 配合超时 context)。
- 避免在中间件里调用
http.Get()、db.QueryRow()等同步阻塞操作 - 本地缓存适合读多写少、key 总量可控的场景(如配置项、字典表)
- Redis 缓存需设置合理
context.WithTimeout,超时直接穿透,别让失败拖垮整个链路










