在SaaS类应用开发中,多租户架构是主流选择,不同租户的数据需要严格隔离,基于PostgreSQL的动态数据库连接管理可以很好地满足这一需求。每个租户对应独立的数据库实例或者独立的数据库schema,应用根据当前请求的租户信息动态切换数据库连接,既保证数据安全,又能合理复用连接资源。
核心设计思路
实现动态数据库连接管理需要解决三个核心问题:租户标识的提取、连接池的缓存管理、连接的动态获取与释放。整体流程是请求进入时先解析租户ID,再从连接池缓存中查找对应租户的连接池,如果不存在则新建连接池并缓存,最后从对应连接池获取连接执行数据库操作。
租户标识提取
租户标识可以通过请求头、JWT令牌、子域名等方式传递,这里以请求头传递租户ID为例,在Go的中间件中提取租户信息:
package middleware
import (
"context"
"net/http"
)
// 定义租户ID的上下文键
type contextKey string
const tenantIDKey contextKey = "tenant_id"
// 提取租户ID的中间件
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取租户ID
tenantID := r.Header.Get("X-Tenant-ID")
if tenantID == "" {
http.Error(w, "缺少租户标识", http.StatusBadRequest)
return
}
// 将租户ID存入请求上下文
ctx := context.WithValue(r.Context(), tenantIDKey, tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 从上下文获取租户ID的辅助函数
func GetTenantID(ctx context.Context) string {
val := ctx.Value(tenantIDKey)
if val == nil {
return ""
}
return val.(string)
}
动态连接池管理
我们需要一个全局的连接池管理器,缓存不同租户对应的PostgreSQL连接池,避免重复创建连接带来的性能损耗。连接池管理器需要支持连接池的创建、获取、过期清理等功能:
package db
import (
"fmt"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// 连接池管理器结构体
type PoolManager struct {
pools map[string]*pgxpool.Pool
mu sync.RWMutex
// 数据库连接配置模板,不同租户替换数据库名即可
dsnTemplate string
}
// 初始化连接池管理器
func NewPoolManager(dsnTemplate string) *PoolManager {
return &PoolManager{
pools: make(map[string]*pgxpool.Pool),
dsnTemplate: dsnTemplate,
}
}
// 根据租户ID获取连接池
func (m *PoolManager) GetPool(tenantID string) (*pgxpool.Pool, error) {
// 先读锁查询缓存
m.mu.RLock()
pool, exists := m.pools[tenantID]
m.mu.RUnlock()
if exists {
return pool, nil
}
// 缓存不存在,加写锁创建新连接池
m.mu.Lock()
defer m.mu.Unlock()
// 二次检查避免重复创建
if pool, exists = m.pools[tenantID]; exists {
return pool, nil
}
// 替换DSN模板中的租户占位符,生成当前租户的DSN
// 假设模板格式为:postgres://user:password@localhost:5432/{tenant}?sslmode=disable
dsn := fmt.Sprintf(m.dsnTemplate, tenantID)
// 创建连接池
pool, err := pgxpool.New(context.Background(), dsn)
if err != nil {
return nil, fmt.Errorf("创建租户%s连接池失败:%v", tenantID, err)
}
m.pools[tenantID] = pool
return pool, nil
}
// 清理过期连接池,可定时调用
func (m *PoolManager) CleanupIdlePools(idleTimeout time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
for tenantID, pool := range m.pools {
// 这里可以根据实际使用情况判断连接池是否空闲,示例简化为直接检查存活状态
if pool.Stat().TotalConns() == 0 {
pool.Close()
delete(m.pools, tenantID)
}
}
}
数据库操作封装
封装统一的数据库操作入口,自动根据当前请求的租户信息获取对应的连接,上层业务不需要关心连接切换的细节:
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
)
// 执行查询操作
func Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
tenantID := GetTenantID(ctx)
if tenantID == "" {
return nil, fmt.Errorf("未获取到租户标识")
}
pool, err := poolManager.GetPool(tenantID)
if err != nil {
return nil, err
}
return pool.Query(ctx, sql, args...)
}
// 执行增删改操作
func Exec(ctx context.Context, sql string, args ...interface{}) (pgx.CommandTag, error) {
tenantID := GetTenantID(ctx)
if tenantID == "" {
return "", fmt.Errorf("未获取到租户标识")
}
pool, err := poolManager.GetPool(tenantID)
if err != nil {
return "", err
}
return pool.Exec(ctx, sql, args...)
}
// 全局连接池管理器实例,初始化时传入DSN模板
var poolManager = NewPoolManager("postgres://postgres:123456@127.0.0.1:5432/%s?sslmode=disable")
完整使用示例
将上面的组件组合起来,实现一个简单的用户查询接口,不同租户查询自己的用户数据:
package main
import (
"encoding/json"
"net/http"
"your_project_path/db"
"your_project_path/middleware"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(r.Context(), "SELECT id, name FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", getUserHandler)
// 使用租户中间件包装路由
handler := middleware.TenantMiddleware(mux)
http.ListenAndServe(":8080", handler)
}
注意事项
- 连接池大小需要根据租户数量和单租户并发量合理设置,避免连接数过多耗尽PostgreSQL的资源。
- 如果采用schema隔离方案,不需要切换数据库,只需要动态设置search_path即可,连接池可以共用,减少连接开销。
- 租户标识的传递需要做好安全校验,避免租户越权访问其他租户的数据。
- 连接池需要定期清理空闲连接,避免长期占用资源。
动态数据库连接管理的核心是根据租户信息精准匹配对应的数据库资源,在隔离性和资源利用率之间找到平衡,结合Go的并发特性可以高效支撑多租户场景的需求。
GoPostgreSQL多租户应用动态数据库连接修改时间:2026-06-27 02:00:51