不能靠单个*gorm.DB切换数据库,每个租户必须使用独立实例,否则事务失效、连接池错乱、日志与回调互相污染;应使用context.WithValue配合唯一key注入租户DB,通过sync.Map缓存并按租户单独配置连接池。

不能靠一个 *gorm.DB 或 *sql.DB 变量“切换”数据库——每个库必须是独立实例,否则事务失效、连接池错乱、日志和回调互相污染。
为什么 echo.Context 里塞一个全局 db 变量就出事
echo 的 c.Set("db", db) 看似方便,但实际是类型不安全的字符串 key,handler 里 c.Get("db") 返回 interface{},强转失败静默或 panic;更严重的是,如果多个租户共用同一个 *gorm.DB 实例,它们的连接池、日志器、回调链全混在一起。比如 userDB.Use(logger.New()) 后,所有走这个变量的查询都会打日志,哪怕本该静默的审计库也跟着刷屏。
- 别用字符串做 context key:定义
var CtxDBKey = struct{}{},再用c.Request().Context() = context.WithValue(ctx, CtxDBKey, db) - 别在中间件里复用已有
*gorm.DB:每次gorm.Open必须传新 DSN,且返回值赋给新变量(如tenantDB) - 首次调用
db.Ping()必须成功,否则缓存了无效实例,后续所有请求都卡在连接建立上
sync.Map 缓存租户 DB 实例的正确姿势
租户 ID 到 *gorm.DB 的映射必须线程安全,且读多写少。用 map[string]*gorm.DB 加 mutex 容易成为性能瓶颈;sync.Map 更合适,但要注意键必须能唯一标识数据源——不能只用租户名,得带上驱动类型和关键配置哈希,比如 fmt.Sprintf("mysql-%s-%d", tenantID, port)。
- 缓存值是
*gorm.DB,不是*sql.DB或原始连接,否则丢了 GORM 的回调和日志能力 - 用
LoadOrStore而非Store:避免重复初始化同一租户的 DB 实例 - DSN 中敏感字段(如密码含
/或@)必须先url.QueryEscape,否则gorm.Open不报错,但第一次Create就返回"invalid connection"
echo 中间件里怎么安全注入 tenant DB
入口中间件负责提取租户标识、校验存在性、加载对应 DB 实例并注入 context。这里最容易漏掉的是:没查主库确认租户有效性,就直接去 sync.Map 里找,结果租户不存在时返回 nil,handler 里又没判空,直接 panic。
立即学习“go语言免费学习笔记(深入)”;
- 从
c.Request().Header.Get("X-Tenant-ID")或 JWT claim 提取租户 ID - 用主库(如
masterDB)查tenants表确认该租户已启用且配置有效 - 查到后调
tenantDBs.LoadOrStore(tenantID, NewTenantDB(cfg)),其中NewTenantDB内部调gorm.Open+SetMaxOpenConns+Ping - 最后
c.SetParam("tenant_id", tenantID)并把 DB 实例塞进 context:c.Request().Context() = context.WithValue(c.Request().Context(), CtxDBKey, db)
handler 里取 DB 实例的唯一合法方式
handler 中绝不能 fallback 到全局 db 变量,也不能用 c.Get("db") 这种弱类型方式。必须显式从 context 取,并做类型断言和空值检查——因为中间件可能因租户未注册、DB 初始化失败等原因没塞值。
- 取值写法固定:
db, ok := c.Request().Context().Value(CtxDBKey).(*gorm.DB) - 必须判断
ok == false或db == nil,此时应返回echo.HTTPError{Code: 503, Message: "tenant db unavailable"} - 事务只能在单个
*gorm.DB内生效:db.Transaction(func(tx *gorm.DB) error { ... }),里面所有操作必须用tx,不能混用其他 DB 实例
最常被忽略的一点:连接池参数(SetMaxOpenConns、SetConnMaxLifetime)必须按租户单独设置。某个租户流量突增,只调大它的连接池,不影响其他租户——这要求每个 *gorm.DB 实例初始化时就完成配置,而不是靠全局变量统一改。


















