不能直接在gin或beego全局db变量中硬编码切换,因为sql.db/gorm.db实例有状态(事务、连接池、钩子等),并发下修改全局变量会破坏安全,导致事务跨库、连接泄漏;正确做法是用context透传租户标识,中间件解析后存入ctx,dao层按key从sync.map缓存中获取对应预加载的独立db实例。

为什么不能直接在 Gin 或 Beego 的全局 DB 变量里硬编码切换?
因为 Go 的数据库连接(比如 *gorm.DB)是**有状态的**:事务、连接池、日志配置、回调钩子都绑定在单个实例上。你无法在一次 HTTP 请求中“临时替换”全局 global.DB,否则会破坏并发安全,也容易导致事务跨库、连接泄漏或日志错乱。
用 context.Context 透传路由决策,而不是改全局变量
真正的动态路由必须把“选哪个库”的逻辑下沉到请求生命周期内,靠 context 携带决策结果,再由 DAO 层按需获取对应实例。常见做法是:
- 在中间件里解析请求路径、Header(如
X-Tenant-ID)、JWT claim 或 Query 参数,提取路由标识(例如tenant_a、shard_2024) - 将标识写入
ctx:ctx = context.WithValue(ctx, dbKey, "tenant_a") - DAO 方法接收
ctx,调用getDBByCtx(ctx)查表(map[string]*gorm.DB)返回对应实例 - 所有
Create/First等操作都基于该实例执行,不碰全局global.DB
注意:context.WithValue 的 key 必须是自定义类型(不能用 string),否则易冲突;map 查表建议加读锁(sync.RWMutex),写库注册时才加写锁。
gorm.Open 实例不能复用,但连接池可以共享
每个 *gorm.DB 实例底层都持有独立的 *sql.DB 连接池。如果为每个租户都调用一次 gorm.Open,会导致大量空闲连接堆积(尤其 MySQL 默认最大连接数有限)。更合理的做法是:
当代理已经知道网站路由或内容URL,并且在启动前需要有效的sitemap XML、sitemap索引或robots.txt引用时,请使用sitemap。这是一个发布构件技能,而不是爬虫或SEO平台。
- 预初始化一组固定数量的
*sql.DB(比如按地域分:cn-east、us-west),复用其连接池 - 用这些
*sql.DB分别创建*gorm.DB实例,但关闭gorm.Config.PrepareStmt和gorm.Config.SkipDefaultTransaction(避免预编译语句跨库污染) - 把
*gorm.DB实例缓存在 map 中,key 是逻辑标识(如"tenant_a@cn-east"),value 是实例指针
这样既隔离了 GORM 层行为(回调、日志、命名空间),又节省了底层连接资源。
Beego 的 orm.RegisterModel 和 Gin 的模型注册不支持跨库自动路由
Beego ORM 的 orm.RegisterModel 或 GORM 的 AutoMigrate 都只作用于单个 *gorm.DB 实例。你不能指望 db.Create(&user) 自动根据 User.TenantID 字段决定往哪个物理库写——这属于业务路由逻辑,必须显式控制。
容易踩的坑:
- 在模型 struct 上加
gorm:"column:xxx"`不会影响库选择,只是字段映射 - 用
db.Table("users").Where(...).Find()仍走当前db实例,不会触发路由 - 事务必须限定在单个
*gorm.DB内,跨库事务只能靠应用层补偿(Saga 模式)
真正需要多库事务时,别试图用 GORM 封装,老老实实拆成多个独立操作 + 幂等 + 补偿任务。










