You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Gorm(PostgreSQL)中定义只读副本及最佳实践

好问题!我刚好在类似的Golang+Gorm+Cloud SQL场景下处理过读写分离配置,给你梳理下可行的方案:

Gorm中配置只读副本自动处理的官方方案

Gorm v2 其实提供了官方的解决方案——DB Resolver插件,专门用来处理读写分离、多数据库以及只读副本的路由逻辑。你可以通过它轻松配置主库(写)和多个只读副本,Gorm会自动帮你把查询请求分发到副本,写请求发到主库,完全不需要手动判断。

对应你的场景,给你写个具体的配置示例:

package main

import (
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
  "gorm.io/plugin/dbresolver"
)

func main() {
  // 主库(写操作)的DSN,替换成你Cloud SQL主库的实际配置
  writeDSN := "host=主库IP user=root password=你的主库密码 dbname=你的数据库名 port=5432 sslmode=require"
  // 两个只读副本的DSN,对应你提供的配置
  readDSNs := []string{
    "host=8.8.8.8 user=anotherusernamethanroot password=lolcats! dbname=你的数据库名 port=5432 sslmode=require",
    "host=localhost user=root dbname=你的数据库名 port=5432 sslmode=require",
  }

  // 先连接主库
  db, err := gorm.Open(postgres.Open(writeDSN), &gorm.Config{})
  if err != nil {
    panic("主库连接失败:" + err.Error())
  }

  // 注册DB Resolver插件,配置读写分离和副本
  err = db.Use(dbresolver.Register(dbresolver.Config{
    // 配置只读副本列表
    Replicas: []gorm.Dialector{
      postgres.Open(readDSNs[0]),
      postgres.Open(readDSNs[1]),
    },
    // 负载均衡策略:默认是随机分配请求到副本,也可以用轮询(RoundRobinPolicy)
    Policy: dbresolver.RandomPolicy{},
  }).SetMaxOpenConns(20).SetMaxIdleConns(10)) // 配置连接池参数
  if err != nil {
    panic("注册DB Resolver失败:" + err.Error())
  }

  // 之后的操作会自动路由:
  // 写操作(Create/Update/Delete/Save等)自动走主库
  db.Create(&YourModel{})
  // 读操作(Find/First/Raw查询等)自动随机分配到两个副本中的一个
  var model YourModel
  db.First(&model, 1)

  // 如果遇到需要强一致性的读操作(比如刚写完就要读),可以强制走主库
  db.Session(&gorm.Session{Resolver: dbresolver.Write}).First(&model, 1)
}

这个插件的核心优势:

  • 自动路由:无需业务代码做额外判断,Gorm内部会根据操作类型分发请求
  • 灵活的负载均衡:支持随机、轮询,还能自定义负载策略
  • 连接池管理:可以独立配置主库和副本的连接池参数
手动实现读写分离的最佳实践(如果不想用官方插件)

如果因为兼容性(比如还在使用Gorm v1)或者自定义需求不想用官方插件,手动实现的核心思路是维护主库和副本的独立DB实例,封装路由逻辑。

简单的实现示例:

package main

import (
  "math/rand"
  "time"

  "gorm.io/driver/postgres"
  "gorm.io/gorm"
)

// 自定义DB包装器,封装读写分离逻辑
type DBWrapper struct {
  writeDB *gorm.DB   // 主库,处理写操作
  readDBs []*gorm.DB // 只读副本列表
  r       *rand.Rand // 用于随机选择副本
}

// 初始化DB包装器
func NewDBWrapper(writeDSN string, readDSNs []string) (*DBWrapper, error) {
  // 初始化主库
  writeDB, err := gorm.Open(postgres.Open(writeDSN), &gorm.Config{})
  if err != nil {
    return nil, err
  }

  // 初始化所有只读副本
  var readDBs []*gorm.DB
  for _, dsn := range readDSNs {
    readDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
      return nil, err
    }
    readDBs = append(readDBs, readDB)
  }

  return &DBWrapper{
    writeDB: writeDB,
    readDBs: readDBs,
    r:       rand.New(rand.NewSource(time.Now().UnixNano())),
  }, nil
}

// 获取随机的只读副本实例
func (w *DBWrapper) ReadDB() *gorm.DB {
  idx := w.r.Intn(len(w.readDBs))
  return w.readDBs[idx]
}

// 获取主库实例(处理写操作)
func (w *DBWrapper) WriteDB() *gorm.DB {
  return w.writeDB
}

// 封装常用的读方法,比如First
func (w *DBWrapper) First(dest interface{}, conds ...interface{}) *gorm.DB {
  return w.ReadDB().First(dest, conds...)
}

// 封装常用的写方法,比如Create
func (w *DBWrapper) Create(value interface{}) *gorm.DB {
  return w.WriteDB().Create(value)
}

业务层使用示例:

func main() {
  writeDSN := "主库DSN"
  readDSNs := []string{"副本1DSN", "副本2DSN"}
  
  wrapper, err := NewDBWrapper(writeDSN, readDSNs)
  if err != nil {
    panic(err)
  }

  // 读操作自动走副本
  var user User
  wrapper.First(&user, 1)

  // 写操作自动走主库
  wrapper.Create(&User{Name: "test_user"})
}

手动方式的好处是完全可控,你可以根据业务需求定制路由规则(比如某些特定查询强制走主库),但缺点是需要自己封装所有需要的Gorm方法,相对繁琐。

额外注意事项
  • 副本延迟问题:Cloud SQL的PostgreSQL副本存在一定的同步延迟,如果你的业务对数据一致性要求极高,记得在必要时强制走主库(官方插件用Session(&gorm.Session{Resolver: dbresolver.Write}),手动方式直接调用WriteDB())。
  • 权限配置:确保你的应用服务器IP已经被添加到Cloud SQL主库和副本的白名单中,或者通过VPC peering等方式建立了可靠的网络连接。

内容的提问来源于stack exchange,提问作者Raghavendra Gautam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:23:44