缓存分页总数反而更慢,因其在实时性要求高或数据高频变更时易导致一致性问题、缓存穿透及显示错误;应优先缓存整页结果或raw sql查询结果,并采用游标分页+带版本key的精准缓存策略。

为什么缓存分页总数反而让接口更慢
直接缓存 Total 字段看似省事,但多数场景下它会引入额外负担:当业务要求“实时总数”(比如后台运营看板),缓存就失效;而如果缓存过期策略太松,又会导致前端显示“共 12345 条”,点开第 618 页却返回空数组——因为真实数据已删了上千条。更麻烦的是,Count(*) 查询本身在百万级表上可能耗时 300ms+,你用 Redis 缓存它,只是把一次慢查询换成了缓存穿透风险。
- 缓存总数只适用于低频变更的数据,如配置项、静态字典表
- 用户生成内容(UGC)、订单、日志类数据,缓存总数大概率带来一致性问题
- 若必须缓存,用带版本号的 key(如
user:count:v2),配合写操作主动 del 或 update,别依赖 TTL 自动过期
GORM 分页结果缓存的三个硬限制
缓存整页查询结果(如 []User)比缓存总数更实用,但也踩过不少坑:GORM 返回的 struct 默认不实现 json.Marshaler,直接 json.Marshal 可能漏掉零值字段;游标分页里 last_id 和排序方向(ASC/DESC)必须作为缓存 key 的一部分,否则 ASC 查完再 DESC 查会命中错误数据。
- key 必须包含全部影响结果的参数:
users:status=active:sort=id:dir=asc:last_id=12345:limit=20 - 不要缓存带关联预加载(
Preload)的结果,因为 GORM 的Association字段在序列化时行为不稳定 - 缓存时间建议 ≤ 30s,高写入场景下超过这个值,重复数据或丢失数据概率显著上升
绕过 GORM 直接缓存 Raw SQL 查询结果
当分页逻辑含复杂 Joins、子查询或聚合字段时,GORM 的 Scan + Rows 流式读取比全量 Find 更适合缓存。关键不是“能不能缓存”,而是“缓存什么”。我们实测发现,对同一分页请求,缓存 db.Raw("SELECT id,name,created_at FROM users WHERE status = ? ORDER BY id LIMIT ? OFFSET ?").Rows() 的二进制结果([]byte),比缓存 GORM model 实例快 2.3 倍,内存占用低 60%。
- 用
sha256.Sum256对原始 SQL + 参数做哈希生成 key,避免 key 过长 - 结果缓存前先
json.Marshal成[]map[string]interface{},不走 GORM model 层 - 注意
OFFSET值不能来自前端直传,必须经校验(如offset := (page - 1) * limit,且page )
游标分页 + 缓存组合的最小安全单元
真正稳定的方案是放弃“页码”,只信任游标,并把游标本身当作缓存锚点。例如前端传 cursor=MTIzNHxhYmNkZWY=(base64 编码的 "1234|abcdef"),后端解码后拆成 lastID=1234 和 lastCreatedAt="abcdef",再拼进 WHERE id > ? AND created_at >= ?。此时缓存 key 就是 users:cursor:MTIzNHxhYmNkZWY=:limit=20 —— 它天然绑定数据快照,不会因中间插入新记录而错位。
- 首次请求无 cursor,不走缓存,查
ORDER BY id ASC LIMIT 21,多取 1 条判断是否有下一页 - 每次缓存必须带过期时间,且过期后不回源查全量,而是降级为“游标重置”:返回前 20 条 + 新 cursor
- 禁止在游标中混用可变字段(如
status),它会导致同个 cursor 在不同请求中匹配到不同行
缓存不是银弹,尤其在分页场景里,它最容易掩盖底层 SQL 设计缺陷。真正要压低延迟,得先确认你的排序字段有索引、游标值没被前端篡改、以及是否真的需要“总页数”这个字段。











