如何在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
相关产品推荐
相关产品推荐

