Echo框架本身不支持读写分离,需通过手动管理多个数据库实例并在handler中按业务意图显式调用主库或从库实现。

直接用 Echo 框架本身无法实现读写分离——它不碰数据库,也不管你连几个 *sql.DB。真正起作用的是你初始化的多个数据库实例 + 显式路由逻辑,Echo 只负责把请求交给你写的 handler,剩下的全得自己扛。
为什么不能在 Echo 中间件里统一做 SQL 路由
中间件是 HTTP 层概念,而读写分离发生在 DAO 或 GORM 层。你在中间件里 parse URL 或 method(比如 GET → 读、POST → 写)看似合理,但会漏掉大量边界情况:
-
POST /users是创建,该走主库;但POST /users/batch-import可能只是触发异步任务,里面全是查从库 -
GET /orders?status=paid看似读,但若加了SELECT ... FOR UPDATE,就必须走主库,否则 MySQL 直接报ERROR 1290 (HY000): The MySQL server is running with the --read-only option - Echo 的
echo.Context不带数据库意图,你没法在中间件里安全地“预判”后续 DAO 调用是否需要强一致
真正在中间件里做路由,等于把业务语义硬编码进 HTTP 层,耦合度高、不可测、难维护。
怎么在 Echo handler 里正确使用两个 *sql.DB 实例
核心原则:handler 不该直接操作 *sql.DB,而应通过封装好的 DAO 接口,由 DAO 决定用哪个实例。但初始化和注入必须干净:
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 在
main()启动时,分别调用sql.Open("mysql", dsnMaster)和sql.Open("mysql", dsnSlave),得到masterDB和slaveDB两个独立变量 - 给每个实例单独调参:
masterDB.SetMaxOpenConns(50)、slaveDB.SetMaxOpenConns(200);masterDB.SetWriteTimeout(2 * time.Second)、slaveDB.SetReadTimeout(8 * time.Second) - 把这两个实例注入到 handler 依赖中(例如用结构体字段或依赖注入容器),避免全局变量或闭包捕获
- DAO 方法签名要显式表达意图,比如
GetUserByID(ctx context.Context, id int64) (*User, error)内部用slaveDB.QueryRow();CreateUser(ctx context.Context, u *User) error内部用masterDB.Exec()
别写这种代码:if strings.HasPrefix(c.Request().Method, "GET") { db = slaveDB } else { db = masterDB }——它会在事务、强一致读、SELECT FOR UPDATE 场景下立刻崩。
GORM + dbresolver 在 Echo 中怎么配才不翻车
如果你用 GORM,dbresolver 插件可以帮你省点路由胶水代码,但它不是银弹,配置错一步就全打到从库:
- 必须在
gorm.Open()后立刻调用db.Use(dbresolver.Register(...)),且Sources里只放主库 DSN,Replicas里只放从库 DSN;写反会导致所有Create都报cannot execute INSERT in a read-only transaction - 事务内自动走主库,但
db.Transaction(func(tx *gorm.DB) error { return tx.First(&u).Error })这种写法没问题;而tx.Raw("SELECT * FROM users").Scan()也走主库——因为tx来自主库实例 - 强一致读必须显式标注:
db.Clauses(dbresolver.Write).Where("id = ?", id).First(&u)或db.Session(&gorm.Session{Write: true}).First(&u);Where().First()默认走从库,哪怕刚Create完 - 别信“GORM 会自动识别
SELECT FOR UPDATE”——它不会。Raw 查询默认走从库,你得手动加.Clauses(dbresolver.Write)
最常被忽略的一点:GORM 的 dbresolver 不感知复制延迟。从库 lag > 10s 时,First() 仍会发过去,前端看到“刚注册就 404”。这不是配置问题,是架构必须面对的现实。
健康检查和故障剔除不能靠 GORM 自带逻辑
GORM 的 dbresolver 没有内置节点健康探测,从库宕机后,请求照发,直到超时失败。生产环境必须自己补:
- 启动时对每个从库
slaveDB.PingContext(),失败则跳过该实例,不加入Replicas列表 - 运行期定期 ping(比如每 30 秒),用
slaveDB.Stats().OpenConnections辅助判断是否卡死 - 发现某个从库连续 3 次 ping 失败,调用
dbresolver.ReplaceReplicas()动态剔除;恢复后再加回 - 主库故障时,不能自动把所有流量切到从库——MySQL 从库默认
super_read_only=ON,强行写会报错;真要切主,得运维介入+ binlog 补偿,不是代码能兜住的
真正难的从来不是“怎么让 SELECT 走从库”,而是想清楚哪条读必须强一致、哪条可以降级、延迟抖动时前端怎么提示、失败后要不要重试主库——这些决策藏在业务逻辑里,不在 Echo 或 GORM 的配置项中。

















