

新闻资讯
技术学院本文介绍一种安全、可扩展的 go 多租户数据库连接管理方案:通过中心化租户元数据(主库)识别租户,并在请求生命周期内按需动态初始化并复用对应租户的 postgresql 连接池。
在 Go 构建的多租户应用中,为每个租户分配独立 PostgreSQL 数据库是常见且推荐的隔离策略。但关键挑战在于:如何在不牺牲性能与安全的前提下,实现请求级的数据库连接动态路由? 直接维护 map[string]*sql.DB 虽简单,却存在连接泄漏、并发竞争、缺乏健康检查及冷启动延迟等风险。更优实践是采用“主库驱动 + 连接池缓存 + 请求上下文绑定”的分层设计。
// 主库初始化(全局单例)
var masterDB *sql.DB
func initMasterDB() error {
db, err := sql.Open("pgx", "host=localhost port=5432 dbname=master user=app password=secret")
if err != nil {
return err
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
masterDB = db
return nil
}
// 租户连接池缓存
var tenantDBs sync.Map // map[string]*sql.DB
// 获取租户 DB(线程安全)
func getTenantDB(subdomain string) (*sql.DB, error) {
if cached, ok := tenantDBs.Load(subdomain); ok {
return cached.(*sql.DB), nil
}
// 查询主库获取租户 DB 配置
var config struct {
Host, Port, Name, User, Password string
}
err := masterDB.QueryRow(
`SELECT db_host, db_port, db_name, db_user, db_password FROM tenants WHERE subdomain = $1`,
subdomain,
).Scan(&config.Host, &config.Port, &config.Name, &config.User, &config.Password)
if err != nil {
return nil, fmt.Errorf("tenant not found or db config error: %w", err)
}
// 构建租户 DSN(生产环境请使用 pgxpool 并启用 TLS)
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s",
config.Host, config.Port, config.Name, config.User, config.Password)
// 初始化租户连接池
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open t
enant DB: %w", err)
}
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(1 * time.Hour)
// 验证连接
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("tenant DB ping failed: %w", err)
}
// 缓存并返回
tenantDBs.Store(subdomain, db)
return db, nil
}
// Gin 中间件示例
func TenantRouter() gin.HandlerFunc {
return func(c *gin.Context) {
host := c.Request.Host // 或解析 subdomain: strings.Split(host, ".")[0]
subdomain := strings.Split(host, ".")[0]
db, err := getTenantDB(subdomain)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "invalid tenant"})
return
}
// 注入 Context,供 Handler 使用
c.Set("tenant_db", db)
c.Next()
}
}
// 在 Handler 中使用
func UserListHandler(c *gin.Context) {
db, _ := c.Get("tenant_db")
rows, err := db.(*sql.DB).Query("SELECT id, name FROM users LIMIT 10")
// ... 处理逻辑
}该方案兼顾开发简洁性与生产可靠性,已在多个高并发 SaaS 服务中验证。核心思想是:将租户发现(主库)、连接池生命周期管理(缓存)、请求上下文绑定(中间件)解耦,各司其职,避免全局状态污染与资源争用。