Echo本身不参与数据库路由,读写分离完全由GORM的dbresolver插件控制;但Echo的请求生命周期、中间件顺序及事务绑定方式直接影响路由生效与强一致读落地,配错一步将导致读操作静默走主库或写操作误发从库报ERROR 1290。

直接说结论:Echo 本身不参与数据库路由,读写分离完全由 GORM 的 dbresolver 插件控制;但 Echo 的请求生命周期、中间件顺序、事务绑定方式,会显著影响主从路由是否生效、强一致读能否落地。配错一步,所有读操作静默走主库,或写操作误发从库报 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
dbresolver.Register 必须在 gorm.Open 之后、Echo 路由注册之前调用
很多人把 db.Use(dbresolver.Register(...)) 放在 gorm.Open 前,或者塞进 Echo 中间件里,结果插件根本没挂载成功——dbresolver.Register 只能作用于已初始化的 *gorm.DB 实例,且必须在任何查询执行前完成注册。
- 正确顺序是:
db := gorm.Open(mysql.Open(masterDSN), &gorm.Config{})→db.Use(dbresolver.Register(...))→ 再传给 Echo 的engine或controllers - 如果在 Echo 的
middleware.DB()中才初始化db,那所有 handler 里的db.Find()都会 fallback 到主库(因为 resolver 没注册) - 验证是否生效:加
db.Debug()后执行一个Find(),看日志里打印的 IP 是不是从库地址;不是,说明注册失败或顺序错了
事务内强制走主库,但 Echo 的 request-scoped 事务容易漏掉 context 传递
只要进了 tx := db.Begin(),里面所有 tx.First()、tx.Where().Scan() 都自动走主库——这是 GORM 的硬规则。但问题在于,Echo 默认不帮你把 tx 注入到 handler 的上下文里,你得自己做。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 常见错误:在 middleware 里
tx := db.Begin(),但没调用c.Set("tx", tx),handler 里直接用全局db查询,结果读从库、看到旧数据 - 修复方式:中间件中
c.Set("tx", tx),handler 里用c.Get("tx").(*gorm.DB)替换默认db;或者更安全地用db.WithContext(c.Request().Context())+ 自定义 Context key - 别依赖
db.Session(&gorm.Session{Context: c.Request().Context()})自动识别事务——它只认context.WithValue(ctx, txKey, *gorm.DB)这种显式注入
Raw SQL 和链式调用混用时,路由策略极易失效
GORM 不解析 SQL 文本,只按方法名硬路由:Create/Update 走主库,Find/First 走从库。但 Raw()、Where().Select().Scan() 这类组合,很容易绕过路由判断。
-
db.Raw("SELECT * FROM users FOR UPDATE").Scan(&u)默认走从库 → 报ERROR 1290;必须显式加.Clauses(dbresolver.Write) -
db.Where("id = ?", id).Select("name").Scan(&name)看似是读,但若中间穿插了.Session(&gorm.Session{Write: true}),顺序错会导致Write策略被后续Scan覆盖 - 最稳写法:
db.Clauses(dbresolver.Read).Where(...).Find()或db.Clauses(dbresolver.Write).Raw(...).Exec(),不依赖隐式规则
从库连接池参数必须独立配置,否则负载不均+连接泄漏
很多人只对主库调用 db.DB().SetMaxOpenConns(30),却忘了每个从库 slaveDB 也要单独设——GORM 的 AddSlave 或 dbresolver.Register 注册的是新连接池,不是复用主库配置。
- 从库连接池建议:比主库高 2–3 倍并发数(如主库 30,单个从库设 80),
SetConnMaxLifetime(15 * time.Minute)避免被 MySQLwait_timeoutkill 后复用失败 - 漏设
SetMaxIdleConns会导致从库空闲连接堆积,SetMaxOpenConns设太小则并发读时频繁建连,触发dial tcp: i/o timeout - 启动时务必检查
AddSlave返回的error:从库地址错、密码错、网络不通,AddSlave不 panic,但后续所有读请求 fallback 主库,监控里看不到异常
真正难的不是配通,而是主从延迟下如何让“刚创建就查看详情”这种场景不出错——GORM 不查 Seconds_Behind_Master,也不自动降级,这层逻辑必须写在业务代码里,比如查不到时主动 fallback 主库重试一次,或用 db.WithContext(context.WithValue(ctx, "force_master", true)) 显式标记。


















