分页查询必须走读库,不可复用写库连接;需用db.Session(&gorm.Session{Replica: true})获取同一从库实例执行Count和Find,避免主从延迟导致数据不一致;游标分页更适配读写分离,须用主键等单调字段并确保游标值来自同从库查询。

分页查询必须走读库,不能复用写库连接
读写分离下,GORM 默认的 db 实例通常指向主库(写库),直接用于分页会把 COUNT(*) 和 LIMIT/OFFSET 查询都打到主库,白白增加主库压力。你得显式切换到只读从库实例做分页——这不是可选项,是必须动作。
常见错误现象:db.Model(&User{}).Count(&total) 返回慢、主库 CPU 突增、从库负载却很低。
- 用
db.Session(&gorm.Session{Replica: true})获取从库 session(GORM v2.2+ 支持),再在其上调用Count和Find - 不要依赖全局
db变量做分页;建议封装一个ReadOnlyDB()函数,内部调用db.Session(...)并返回新实例 - 若用自定义路由(如基于
gorm.io/plugin/dbresolver),确认Replica策略已启用且从库节点健康,否则 fallback 到主库时不会报错,但失去读写分离意义
Count 查询和分页主查必须用同一从库实例
你以为两次 db.Session(&gorm.Session{Replica: true}) 调用就稳了?不一定。GORM 的 replica 选择是“每次 session 创建时随机选一个可用从库”,两次调用可能落到不同从库节点,而 MySQL 主从延迟会导致 COUNT(*) 和实际数据不一致——比如总数查的是 A 从库(延迟 200ms),主查落到 B 从库(延迟 50ms),结果页数错乱或漏数据。
正确做法是:一次获取从库 session,复用它执行 Count + Find。
- 写法示例:
roDB := db.Session(&gorm.Session{Replica: true}) roDB.Model(&User{}).Where("status = ?", "active").Count(&total) roDB.Where("status = ?", "active").Order("id ASC").Offset((page-1)*pageSize).Limit(pageSize).Find(&users) - 避免在
roDB上叠加Transaction或Debug,这些会干扰 replica 行为 - 如果业务对一致性要求极高(比如财务类分页),宁可牺牲一点性能,也应强制走主库——但此时要评估主库扛不扛得住
游标分页比 Offset 分页更适合读写分离场景
Offset 分页在读写分离下问题更隐蔽:主从延迟导致同一页反复翻出重复/跳过记录,尤其当排序字段(如 created_at)在从库尚未同步时。游标分页靠 WHERE 条件定位,天然规避 OFFSET 扫描 + 主从延迟双重陷阱。
但它对 SQL 构造和前端传参有硬性要求,不是简单换函数就能跑通。
- 必须用带索引的单调字段做游标,首选主键
id(自增、无空洞、强顺序),次选created_at DESC, id DESC(需联合索引) - 前端首次请求不带游标参数,后端查
ORDER BY id ASC LIMIT 20,取users[len(users)-1].ID作为next_cursor返回;后续请求带?cursor=12345,后端生成WHERE id > 12345 ORDER BY id ASC LIMIT 20 - 游标分页无法直接返回总条数,
Count(*)失去意义;若前端必须显示“共 N 条”,只能预估或改用带has_next的布尔分页 - 别在游标查询里混用
Preload,关联数据必须拆成两步:先查主表 ID 列表,再用IN批量加载,否则从库延迟会让关联结果严重失真
Preload 关联分页在读写分离下几乎不可用
如果你在分页查询中写了 Preload("Profile"),GORM 会先查用户,再对每个用户发一条关联查询(N+1),而这些 Preload 查询默认走主库(除非你显式指定 Session)。结果就是:主查走从库,Preload 全打主库,主库瞬间被压垮,且从库查到的用户 ID 在 Preload 时可能已被主库删掉或变更——数据错乱。
更糟的是,Preload + Offset 组合在读写分离下毫无事务语义,完全不可控。
- 彻底禁用分页中的
Preload;改为先查主表 ID 列表(走从库),再用db.Where("user_id IN ?", ids).Find(&profiles)批量查关联(可选主库或从库,但必须统一) - 如果关联数据也要分页(比如每个用户只取最新 3 个订单),必须用子查询或窗口函数,不能依赖 GORM 的链式
Preload - 测试阶段务必开启 GORM
Debug模式,确认所有 SQL 都落在预期库上;生产环境用数据库代理层(如 ProxySQL)监控实际路由路径


















