GO语言使用gorm的dbresolver插件实现数据库读写分离
settings.yaml文件中读库跟写库的配置信息:db:读库,db1:写库
yaml
db:
user: root
password: 123456
host: 127.0.0.1
port: 3306
db: blogx
debug: false
source: mysql
db1:
user: root
password: 123456
host: 127.0.0.1
port: 3306
db: test
debug: false
source: mysql
conf/enter.go
go
type Config struct {
System System `yaml:"system"`
Log Log `yaml:"log"`
DB DB `yaml:"db"` //读库
DB1 DB `yaml:"db1"` //写库
}
conf/conf_db.go
go
type DB struct {
User string `yaml:"user"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port int `yaml:"port"`
DB string `yaml:"db"`
Debug bool `yaml:"debug"` //打印全部日志
Source string `yaml:"source"` //数据库的源 MySQL pgsql
}
func (d DB) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",
d.User, d.Password, d.Host, d.Port, d.DB)
}
func (d DB) Empty() bool {
return d.User == "" && d.Password == "" && d.Host == "" && d.Port == 0
}
core/init_db.go: 利用 GORM 的 dbresolver 插件,把两份配置注册成 "Sources / Replicas"
go
import (
"blogx_server/global"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/plugin/dbresolver"
"time"
)
func InitDB() *gorm.DB {
dc := global.Config.DB //读库
dc1 := global.Config.DB1 //写库
db, err := gorm.Open(mysql.Open(dc.DSN()), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, // 表迁移时不自动创建外键约束
})
if err != nil {
panic(err)
}
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10) // 空闲连接池大小
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
logrus.Info("数据库连接成功")
if !dc1.Empty() {
//读写库不为空,就注册读写分离的配置
err = db.Use(dbresolver.Register(dbresolver.Config{
Sources: []gorm.Dialector{mysql.Open(dc1.DSN())}, //写
Replicas: []gorm.Dialector{mysql.Open(dc.DSN())}, //读
Policy: dbresolver.RandomPolicy{},
}))
if err != nil {
logrus.Fatalf("读写配置错误:%s", err)
}
}
return db
}
项目的"数据库读写分离"功能依赖 3 处代码:
- 配置层
•settings.yaml
里的db:
(读库)和db1:
(写库)
•conf/conf_db.go
里的DB
结构体 +Empty()
方法 - 启动层
•core/init_db.go
中利用 GORM 的dbresolver
插件,把两份配置注册成 "Sources / Replicas" - 业务层
• 业务代码照常用db.Create / db.Find...
,GORM 自动把写请求导向写库,把读请求导向读库。
下面循序渐进地把每一步拆开讲。
──────────────────────────────────────
一、YAML + 结构体:怎样描述两套数据库
-
settings.yaml(缩写示例)
db: # 读库
user: root
password: 123456
host: 127.0.0.1
port: 3306
db: blogx
debug: false
source: mysqldb1: # 写库
user: root
password: 123456
host: 192.168.1.10
port: 3306
db: blogx
debug: false
source: mysql -
conf/enter.go
go
type Config struct {
System System `yaml:"system"`
Log Log `yaml:"log"`
DB DB `yaml:"db"` // 读
DB1 DB `yaml:"db1"` // 写
}
- conf/conf_db.go
•DB.DSN()
把账号/主机/库名拼成 MySQL 连接串;
•DB.Empty()
判断该配置是否完全为空(四项都没填);后面用于"若用户没配写库就别启动读写分离"。
──────────────────────────────────────
二、InitDB:用 dbresolver 把"读 / 写"连起来
核心流程位于 core/init_db.go
:
go
dc := global.Config.DB // 读库
dc1 := global.Config.DB1 // 写库
db, _ := gorm.Open(mysql.Open(dc.DSN()), &gorm.Config{...}) // 先连读库
...
if !dc1.Empty() { // 写库不为空才开启读写分离
err = db.Use(dbresolver.Register(dbresolver.Config{
Sources: []gorm.Dialector{mysql.Open(dc1.DSN())}, // 写
Replicas: []gorm.Dialector{mysql.Open(dc.DSN())}, // 读
Policy: dbresolver.RandomPolicy{}, // 读负载均衡策略
}))
...
}
细节说明:
-
先用读库 DSN 建立基础连接------这样即便只配一套库,也能正常跑起来。
-
判断写库是否留空 ------
dc1.Empty()
会返回 true/false:• 如果用户没有在 YAML 里填
db1:
,代码就不会注册dbresolver
,整个项目退化成"单库"模式。• 如果配了,才执行
db.Use(...)
,将两套连接池交给插件管理。 -
Sources / Replicas 的含义
•
Sources
:主库(写库)。GORM 会把所有会写入数据的操作(Create、Update、Delete、Exec、事务)发送到这里。•
Replicas
:从库(读库)。所有纯查询(First、Find、Scan、Raw SQL 查询)会走这里。 -
Policy :当有多台从库时负责负载均衡。此处用
RandomPolicy
,即随机挑一台;GORM 还内置RoundRobinPolicy
等策略。 -
错误处理 :如果
db.Use
返回 err,立即logrus.Fatalf(...)
终止启动,避免带错配置上线。
──────────────────────────────────────
三、插件工作原理(你不需要额外代码)
dbresolver
在内部注册了一个 callbacks
链。当你后续写下
go
db.First(&user, 1) // 纯查询
db.Create(&order) // 写入
它会自动根据 SQL 操作类别,把请求路由到:
• Replicas
(读池) → user
查询
• Sources
(写池) → order
插入
事务(db.Transaction(func(tx *gorm.DB) {...})
)默认锁定在 Sources
,保证读写一致性。
──────────────────────────────────────
四、连接池参数与日志
连接池大小、生命周期在 3 行里统一设置(对读库池生效;写库池由 dbresolver 使用默认值,也可再通过 Configure
调整):
go
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
成功后 logrus.Info("数据库连接成功")
会在控制台彩显,并写到每日分割的文件里。
──────────────────────────────────────
五、如何"一键切换"读写分离
-
本地/测试环境想省事------只留
db:
、删掉或注释db1:
:•
Empty()
返回 true• InitDB 不注册 dbresolver → 整个程序只使用单库
-
生产环境要上读写分离------补全
db1:
:• 下次启动时
Empty()
变 false• dbresolver 生效,写库指向
db1
,读库指向db
代码无需改动,只改 YAML 即可完成模式切换。
──────────────────────────────────────
六、重点回顾(记住 4 个名词即可)
- DB / DB1 :YAML 中两套连接参数,分别映射到
conf.DB
结构体 - Empty():用来检测写库是否配置
- dbresolver.Register:把 Sources(写库)和 Replicas(读库)注册进 GORM
- RandomPolicy:多读库时的负载均衡策略
如此,"读写分离"在项目中就靠这短短十几行实现,既灵活(配置即开关),又安全(配置缺失时自动回退单库)。