DB连接池必须全局复用且仅在程序退出时关闭,中间件中调用db.Close()会提前销毁整个池;SetMaxOpenConns须≤数据库max_connections并配平SetMaxIdleConns;多租户需按租户ID隔离缓存连接池。

DB 连接池不能在中间件里 Close()
很多人看到 db.Close() 就下意识觉得“用完要关”,但在 Echo 中,sql.DB 是连接池对象,不是单次连接。中间件每次请求都调用 defer db.Close(),会导致整个池被提前关闭,后续所有请求都会卡在 waiting for connection 状态。
真正该做的,是把 db 当作全局长生命周期资源,在应用启动时初始化,在进程退出前统一关闭。比如:
func main() {
db := initDB() // 初始化连接池
defer db.Close() // 仅此处一次,程序退出时执行
e := echo.New()
e.GET("/users", handler(db))
e.Start(":8080")
}
SetMaxOpenConns 和 SetMaxIdleConns 必须配平
连接池参数不匹配是高频性能事故源头。常见错误是把 SetMaxOpenConns 设得过大(比如 1000),但数据库服务端只配了 200 连接数,结果大量请求排队等待,响应延迟飙升。
-
SetMaxOpenConns应 ≤ 数据库服务器的max_connections(留出 DBA、备份等余量) -
SetMaxIdleConns建议设为SetMaxOpenConns的 1/2 ~ 2/3,避免空闲连接长期占着不放又不释放 - 高并发服务建议按 CPU 核数 × 2 ~ 5 设置
SetMaxOpenConns,而非拍脑袋填大数
多租户场景下不能复用同一个 DB 实例
如果业务支持多租户且每个租户有独立数据库(如按 schema 或实例隔离),直接复用一个 sql.DB 会引发连接错乱或权限错误。此时必须为每个租户维护独立连接池。
但要注意:不能为每个租户每次都 new 一个 sql.DB,否则连接数爆炸。正确做法是用 sync.Pool 缓存租户级连接池,Pool 的 Key 必须含租户唯一标识(如 tenant_id 或 schema_name),否则不同租户可能取到彼此的池。
更关键的是:这些租户池的 Close() 也必须由上层统一管理,不能依赖 HTTP 请求生命周期。
HTTPErrorHandler 里别偷偷 close DB
有些人在自定义错误处理时,看到数据库报错就顺手加了 db.Close() ——这是典型误操作。错误处理函数只是响应阶段逻辑,和连接池生命周期完全无关。
只要 db 在 main() 或初始化模块中完成 defer db.Close(),它就会活到进程终止。任何 handler、中间件、error handler 都不该碰 Close()。
真正容易被忽略的点是:连接池本身不感知业务上下文,它只管复用和保活。你认为“这个租户今天没请求,该回收它的池了”——这种想法在连接池层面没有意义;回收动作只发生在进程退出或显式调用 Close() 时。











