gorm不支持自动读写分离,需手动创建主从db实例并显式路由:读操作用dbreplica(配合游标分页防复制延迟错乱),写操作用dbmaster;session/withcontext无法控制路由,preload需手动指定副本实例。

GORM 本身不内置读写分离或分页负载均衡能力,所谓“在只读副本集群下做分页负载均衡”,本质是手动控制查询路由 + 分页逻辑适配,否则 db.Offset().Limit() 会直接打到主库或随机节点,失去意义。
如何让 GORM 查询自动走只读副本
必须显式创建多个 *gorm.DB 实例并手动分发查询,GORM 没有“读策略自动切换”机制。常见错误是只配一个 db 然后幻想它能识别 SELECT 自动切读库。
- 用
gorm.Open()分别连接主库和只读副本(多个dsn),各自封装成不同变量,例如dbMaster和dbReplica - 对分页类
SELECT查询,统一使用dbReplica实例;写操作、事务、SELECT FOR UPDATE必须用dbMaster - 不要依赖
db.Session(&gorm.Session{Read: true})—— 这个字段在 GORM v2 中已被移除,且从未触发实际路由 - 若需动态选副本(比如按负载或延迟),得自己实现一个简易负载均衡器,例如轮询或最小连接数,再把请求分发给对应
dbReplica实例
OFFSET/LIMIT 在副本间分页失效的根本原因
MySQL/PostgreSQL 的 MVCC 机制导致各副本存在复制延迟,同一时刻执行 SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100,主库和副本返回的结果可能不一致——不是“负载不均”,而是“结果错乱”。
- 分页必须基于游标(cursor-based pagination),而非偏移量。用
WHERE id > ? ORDER BY id LIMIT 20替代OFFSET 100 - 游标值(如上例中的
id)必须来自上一页最后一条记录,不能由客户端任意传入,否则跳过或重复 - 若业务强依赖
OFFSET(如管理后台),至少确保所有只读查询都落在同一个副本上,避免跨副本分页;可用 sticky session 或固定副本 ID 路由 - GORM 不提供跨副本一致性分页抽象,
Count()和Find()必须在同一副本执行,否则总数和列表可能对不上
为什么不能靠 GORM 的 Session 或 WithContext 控制读库
GORM 的 Session 只影响事务作用域、日志开关、DryRun 等行为,不改变底层 *sql.DB 连接目标;WithContext 更只是透传 context,和数据库路由完全无关。
-
db.Session(&gorm.Session{NewDB: true})创建的是新会话对象,但底层仍复用原*sql.DB连接池 - 所谓“读写分离中间件”(如某些第三方库)本质是拦截 SQL 类型再路由,但 GORM 的预编译语句、关联加载(
Preload)、事务内查询等场景极易漏判 - 最稳妥的方式仍是业务层明确区分:查列表 →
dbReplica,改数据 →dbMaster,绝不混用 - 如果用了
Preload关联查询,注意它默认复用主查询的 DB 实例——你得手动指定:dbReplica.Preload("Orders", func(db *gorm.DB) *gorm.DB { return dbReplica })
真正难的不是让分页走副本,而是保证分页结果在副本之间可重现。游标分页 + 固定副本路由 + 主键升序排序,这三者缺一不可;任何试图让 GORM “自动聪明”处理读负载的想法,都会在高并发或复制延迟时暴露问题。











