GORM 本身不提供缓存分页能力,需在业务层分离缓存元数据(总条数、分页配置)和页面内容(按页码+条件哈希缓存),并严格控制失效策略,避免 Offset 分页的性能与一致性问题。

直接说结论:GORM 本身不提供缓存分页能力,所谓“结合缓存实现近实时分页”,本质是手动绕过 GORM 的 Offset/Limit 链式调用,在业务层做两件事——把分页元数据(总条数、页码映射)和热门页内容分别缓存,且必须控制好缓存失效策略。
为什么不能直接缓存 db.Offset().Limit().Find() 的结果
因为 Find() 返回的是具体切片(如 []User),缓存它只对「固定页码+固定条件」有效;一旦用户跳转到未缓存的页、或排序字段变更、或有新数据插入,缓存就立刻过期,还可能返回陈旧数据。更麻烦的是,Offset 值越大,查询越慢,缓存命中率反而越低——你花内存存了个高延迟才拿到的结果,得不偿失。
- 缓存单页数据仅适合访问高度集中的首页/热榜页(比如第1页被刷1000次,第97页没人点)
- 总记录数
Count()可以缓存,但必须配合写操作清缓存(如Create/Delete后触发DEL cache:total:user) - GORM 没有内置“缓存感知的分页构造器”,所有缓存逻辑都得自己在 DAO 层外封装
推荐做法:分离「元数据缓存」和「页面内容缓存」
把分页拆成两个可独立缓存的单元:一是总条数与分页配置(如每页20条),二是按页码 + 条件哈希生成的键来缓存具体内容。例如:
// 缓存键示例
cache:Get("page:users:sort=created_at_desc:page=1:limit=20") // 页面内容
cache:Get("count:users:where=status=active") // 总数
- 总数缓存建议 TTL 设为 10–60 秒,足够平衡一致性与压力;更新时用
DEL而非SET,避免缓存穿透 - 页面内容缓存 TTL 应更短(2–10 秒),尤其当数据高频变更;键中必须包含排序字段、过滤条件、limit 值的完整哈希,否则不同参数会互相覆盖
- 首次未命中时,用 GORM 执行真实查询,并用
RowsAffected校验是否真有数据,再写入缓存
游标分页 + 缓存才是近实时的合理组合
传统 OFFSET 分页无法做到“近实时”,因为大偏移量下数据库必须扫描大量行。而游标分页(如 WHERE created_at )天然支持缓存最后一组游标值,且每次查询都是高效索引查找。
- 缓存结构可设计为:
cursor:users:sort=created_at_desc:last_cursor=2024-01-01T00:00:00Z_12345 - 用户翻页时,先查缓存游标,再拼 WHERE 条件交给 GORM 查询,结果集天然有序、无跳页偏差
- 写入新数据后,只需清掉“当前活跃游标”的缓存,不影响已发出的分页请求;比清整个分页缓存更精准
- 注意:GORM 不自动帮你生成游标 WHERE 条件,得自己解析上一页最后一条记录的字段值
容易被忽略的三个细节
第一,CacheFindId 这类方法(如 goweb3 中的)只适用于单条记录,对分页无效——它没解决“第 N 页该取哪几条”的问题;第二,Redis Pipeline 批量读缓存时,别把 count 和 page:N 放进同一 pipeline,前者可能 miss 后要 fallback 查库,后者不能阻塞;第三,任何缓存方案都救不了没建对索引的查询,确保 ORDER BY 字段 + 查询条件字段有联合索引,否则缓存再快,底层 SQL 也慢。











