如何通过GORM DBResolver为多数据源复用单个连接池
问题描述
我使用GORM DBResolver对接多数据源(非副本架构),当前实现代码如下:
var databases = []string{"db_tenant1", "db_tenant2", "db_tenant3"} connString := "postgres://user:password@localhost:5432/postgres?sslmode=disable" db, err := gorm.Open(postgres.Open(connString), &gorm.Config{}) require.NoError(t, err) dr := &dbresolver.DBResolver{} for _, dbname := range databases { connString := fmt.Sprintf("postgres://%s:%s@localhost:5432/%s?sslmode=disable", "user", "password", dbname) dr = dr.Register(dbresolver.Config{ Sources: []gorm.Dialector{postgres.Open(connString)}, Policy: dbresolver.RandomPolicy{}, }, dbname, &TheObject{}) } err = db.Use(dr.SetMaxOpenConns(1)) require.NoError(t, err) for idx, dbname := range databases { result := db.Clauses(dbresolver.Use(dbname), dbresolver.Write).Create(TheObject{}) require.NoError(t, result.Error) } require.NoError(t, err)
执行SELECT * FROM pg_stat_activity后发现GORM为每个数据库创建了独立连接:
12379 postgres 3616 16496 db_tenant1 3617 16606 db_tenant2 3618 16716 db_tenant3 3619
我希望所有数据库(模式)能复用单个连接池中的连接。
解决方案
你现在的问题出在:每个租户数据库都通过独立的postgres.Open()创建了新的Dialector,每个Dialector对应独立的连接池,自然会生成多个独立连接。要复用连接池,核心就是共用同一个底层连接,通过切换数据库/模式来访问不同租户的数据,具体可以这么做:
方案一:用PostgreSQL的SET search_path切换Schema(如果租户用Schema隔离)
如果db_tenant1这些是PostgreSQL的Schema而非独立数据库,根本不用给每个Schema注册数据源,初始连接到主库就行,操作前切换Schema即可:
var schemas = []string{"db_tenant1", "db_tenant2", "db_tenant3"} connString := "postgres://user:password@localhost:5432/postgres?sslmode=disable" db, err := gorm.Open(postgres.Open(connString), &gorm.Config{}) require.NoError(t, err) // 配置连接池,这里只开1个连接 sqlDB, err := db.DB() require.NoError(t, err) sqlDB.SetMaxOpenConns(1) for _, schema := range schemas { // 切换到目标Schema if err := db.Exec("SET search_path TO ?", schema).Error; err != nil { require.NoError(t, err) } // 执行创建操作 result := db.Create(&TheObject{}) require.NoError(t, result.Error) }
方案二:复用连接池,动态切换独立数据库
如果确实是完全独立的数据库,可以通过自定义DBResolver的回调,拿到连接后切换数据库,复用同一个连接池:
var databases = []string{"db_tenant1", "db_tenant2", "db_tenant3"} connString := "postgres://user:password@localhost:5432/postgres?sslmode=disable" db, err := gorm.Open(postgres.Open(connString), &gorm.Config{}) require.NoError(t, err) // 配置全局连接池,只开1个连接 sqlDB, err := db.DB() require.NoError(t, err) sqlDB.SetMaxOpenConns(1) // 只注册一个数据源,通过Resolver回调确定要使用的数据库 dr := dbresolver.Register(dbresolver.Config{ Sources: []gorm.Dialector{postgres.Open(connString)}, Resolver: dbresolver.ResolverFunc(func(destination interface{}, op dbresolver.Operation) string { // 实际业务中可以从上下文、请求参数等地方获取当前租户的数据库名 // 这里示例用上下文传递,你可以根据自己的逻辑调整 ctx := context.Background() return ctx.Value("tenant_db").(string) }), ConnPool: sqlDB, // 强制复用同一个连接池 }).SetMaxOpenConns(1) if err := db.Use(dr); err != nil { require.NoError(t, err) } // 循环操作每个租户数据库 for _, dbname := range databases { ctx := context.WithValue(context.Background(), "tenant_db", dbname) // 先切换到目标数据库 if err := db.WithContext(ctx).Exec("CONNECT TO ?", dbname).Error; err != nil { require.NoError(t, err) } // 执行创建操作 result := db.WithContext(ctx).Create(&TheObject{}) require.NoError(t, result.Error) }
注意点
- 别再为每个租户创建独立的Dialector了,这是产生多连接的根本原因。
- PostgreSQL里切换独立数据库用
CONNECT TO dbname,切换Schema用SET search_path,选哪种取决于你的租户隔离方式。 - 连接池参数一定要设置在底层的
sql.DB实例上,而不是DBResolver的每个数据源配置里。
内容的提问来源于stack exchange,提问作者mic
相关产品推荐
相关产品推荐

