事务内所有操作必须走主库,dbresolver不生效;读操作需显式加Clauses(dbresolver.Write)或Session(&gorm.Session{Write: true})强制主库,否则因主从延迟导致查不到刚写入数据。

事务内所有操作强制走主库,无法中途切从库
GORM 的事务(Begin / BeginTx)一旦开启,后续所有操作——包括 tx.First()、tx.Where().Scan()、tx.Exec() ——都绑定在该事务对应的主库连接上。dbresolver 插件此时完全不生效,也不会尝试把查询路由到从库。
常见错误是封装了「读方法」如 GetUserByID,并在事务函数里直接调用它,结果底层仍走从库,刚写入的数据查不到,或直接报错 ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction。
- 事务必须基于主库
*gorm.DB实例开启,不能用从库实例调Begin() -
tx对象本身不支持切换目标库,它的Query/QueryRow都复用原始连接 - 不要在事务中调用任何封装的「只读」函数,除非你明确传入的是
tx而非全局db
强一致读不能依赖事务自动兜底,得手动标注
你以为「刚 Create 完立刻 First」会读到最新数据?不一定。因为 First 默认走从库,而主从延迟可能达数百毫秒。GORM 不会因你在同一函数里写了写+读就自动把后者切回主库。
正确做法是显式声明读操作的强一致性意图:
- 用
db.Clauses(dbresolver.Write).First(&u)强制走主库 - 或用
db.Session(&gorm.Session{Write: true}).First(&u),注意Session必须放在链式调用最开头,Where().Session().First()会失效 - 避免在事务外用
db.WithContext(ctx)混淆意图;上下文值不会被 dbresolver 解析,必须靠Clauses或Session
Raw 查询在事务中默认安全,但独立使用时极易踩坑
Raw("SELECT * FROM users") 在事务内执行没问题,因为它走的是 tx 连接;但单独调用 db.Raw("SELECT ... FOR UPDATE").Scan() 就危险了——它默认走从库,会触发 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
根本原因:GORM 不解析 SQL 字符串,只看调用方式。带锁查询本质是写操作,但 Raw 方法名不带语义,所以归为「默认读」。
- 所有含
FOR UPDATE、LOCK IN SHARE MODE的Raw,必须加.Clauses(dbresolver.Write) -
Raw("INSERT INTO ... SELECT ...")同理,哪怕以SELECT开头,也得强制走主库 - 别信正则匹配前缀,
strings.HasPrefix(sql, "SELECT")在注释、大小写、子查询面前毫无意义
事务 + 从库延迟 = 业务层必须自己设防
GORM 对 Seconds_Behind_Master 完全无感,也不轮询 SHOW SLAVE STATUS。注册进 Replicas 的从库,只要地址通、能 Ping(),就视为可用,哪怕延迟已到 30 秒。
这意味着:用户注册后跳转个人页,Find 到空数据,不是插件 bug,而是架构现实。
- 关键路径写完后,用
db.Clauses(dbresolver.Write).First()主动确认落库 - 对非实时场景,用 Redis 缓存新写入 ID,并设置短 TTL(如 2s),读取时先查缓存再查库
- 绝不依赖从库做唯一性校验(如邮箱是否存在),这类逻辑必须走主库


















