必须用gorm.Open初始化主库再显式注册dbresolver插件,主库处理写和事务内操作,从库处理默认读请求;强一致读需db.Session({Write: true}),主从延迟须业务层兜底。

怎么用 GORM 的 Database Resolver 插件做读写分离
GORM 官方 Database Resolver 插件是目前 Gin 项目里最轻量、最可控的读写分离方案,不用引入 Amoeba 或 ProxySQL 这类中间件。它本质是在 gorm.Open 后注册多个数据库实例(主库写、从库读),再通过钩子自动路由 SQL 类型。
关键点不是“配多个 DSN”,而是让 GORM 知道哪条 SQL 该走哪个连接。默认规则是:SELECT 走从库,INSERT/UPDATE/DELETE 走主库,事务内强制走主库。
- 必须用
gorm.Open初始化主库连接,再用resolver.Register注册从库切片,不能直接 new 多个*gorm.DB - 从库列表支持负载均衡策略,比如
resolver.Random或resolver.RoundRobin,默认是随机选一个 - 如果某个从库连不上,GORM 不会 panic,但会跳过它继续尝试下一个;你得自己加健康检查逻辑
- 注意
resolver插件只影响Find、First、Count等读操作,手动写的RawSQL 默认走主库,除非显式调用Session(&gorm.Session{DryRun: true})指定
主库和从库的 DSN 配置差异在哪
主库和从库的 DSN 表面看只是 host 和 port 不同,但实际有三个隐性差异点容易导致连接失败或行为异常:
-
read_only=1必须在从库 MySQL 配置里开启,否则UPDATE可能误发到从库并报错ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction - 主库 DSN 推荐加
write_timeout=5s,从库 DSN 加read_timeout=3s,避免慢查询拖垮整个读链路 - 字符集要一致,尤其注意
charset=utf8mb4和collation=utf8mb4_unicode_ci两边都显式声明,否则 GORM 自动迁移时可能因排序规则不一致失败
示例 DSN 片段:
主库:"user:pass@tcp(192.168.1.10:3306)/db?charset=utf8mb4&parseTime=true&loc=Local&write_timeout=5s"
从库:"user:pass@tcp(192.168.1.20:3306)/db?charset=utf8mb4&parseTime=true&loc=Local&read_timeout=3s"
为什么事务里所有操作都走主库
这是 Database Resolver 的硬性设计,不是 bug。只要你调用了 DB.Begin() 或 DB.Transaction(),后续所有 Create、Save、First 都会绑定到同一个主库连接上。
这带来两个现实问题:
- 想在事务里查最新数据(比如刚插入后立刻
SELECT),没问题——因为主库强一致性 - 但如果想“先查从库判断状态,再决定是否写主库”,就不能套在同一个事务里,得拆成两步:先用
DB.WithContext(context.WithValue(ctx, "resolver", "replica"))显式指定读从库,再开事务写主库 - 跨库事务(比如主库写 A 表、从库查 B 表)在 GORM 层不被支持,得靠应用层手动控制,或者换用分布式事务框架
连接池参数不统一会导致什么
主库和从库的 sql.DB 连接池参数(SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime)必须分开设置。共用一套参数容易出问题:
- 主库写压力大,
SetMaxOpenConns设太小会排队;从库读并发高,设太大又浪费内存 - 从库连接空闲时间通常更长,
SetConnMaxIdleTime建议设为 30m,主库建议 5m,避免从库连接被意外回收后重连风暴 - 如果没显式调用
DB.Config.ConnPool.(*sql.DB).SetMaxOpenConns,GORM 默认值是 100,对高并发读场景明显不够
最容易被忽略的是:resolver 创建的从库连接池,不会自动继承主库的配置,必须对每个从库实例单独调用 SetMaxOpenConns。


















