Mastering GORM DBResolver: Read/Write Splitting and Load Balancing in Go
Learn how to use GORM's DBResolver plugin in Go to achieve efficient read/write separation and load balancing across master and replica databases, with detailed code examples, configuration tips, production best practices, and strategies for connection pooling, transaction consistency, and handling replication lag.
Why DBResolver?
In high‑concurrency production environments a single database often becomes a bottleneck because write traffic overloads the master, read traffic competes for the same resources, and replication lag can cause stale reads.
Write pressure : all writes go to the master.
Read pressure : routing reads to the master creates contention.
Replication delay : immediate reads after a write may return old data.
The solution is read/write separation, where the master handles writes and replicas handle reads. GORM’s DBResolver plugin implements this pattern.
Core Capabilities of DBResolver
Read/Write splitting : writes automatically use the master, reads automatically use replicas.
Multi‑database support : bind different models or tables to different data sources.
Load balancing : built‑in random policy with optional custom round‑robin or weighted strategies.
Transaction consistency : a transaction binds to a single connection, preventing switches between master and replica.
Native SQL routing : SELECT statements are sent to replicas, while INSERT/UPDATE/DELETE go to the master.
Quick Start
Assume one master and two replicas:
master: master:3306 replica1: replica1:3306 replica2:
replica2:3306Initialization Code
package database
import (
"log"
"os"
"sync/atomic"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/plugin/dbresolver"
)
var DB *gorm.DB
type RoundRobinPolicy struct {
count uint64
}
func (r *RoundRobinPolicy) Resolve(connPools []gorm.ConnPool) gorm.ConnPool {
n := atomic.AddUint64(&r.count, 1)
return connPools[n%uint64(len(connPools))]
}
func InitDB() {
masterDSN := "root:pass@tcp(master:3306)/appdb?charset=utf8mb4&parseTime=True&loc=Local"
replica1DSN := "root:pass@tcp(replica1:3306)/appdb?charset=utf8mb4&parseTime=True&loc=Local"
replica2DSN := "root:pass@tcp(replica2:3306)/appdb?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "
", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Warn,
Colorful: true,
},
)
var err error
DB, err = gorm.Open(mysql.Open(masterDSN), &gorm.Config{Logger: newLogger})
if err != nil {
panic("connect master failed: " + err.Error())
}
err = DB.Use(
dbresolver.Register(dbresolver.Config{
Sources: []gorm.Dialector{mysql.Open(masterDSN)},
Replicas: []gorm.Dialector{mysql.Open(replica1DSN), mysql.Open(replica2DSN)},
Policy: dbresolver.RandomPolicy{}, // or &RoundRobinPolicy{}
TraceResolverMode: true,
}).
SetConnMaxIdleTime(time.Minute * 5).
SetConnMaxLifetime(time.Hour).
SetMaxIdleConns(10).
SetMaxOpenConns(100),
)
if err != nil {
panic("register dbresolver failed: " + err.Error())
}
log.Println("✅ Database initialized with master + replicas")
}Usage Example
package main
import (
"fmt"
"gorm.io/plugin/dbresolver"
"myapp/internal/database"
)
type User struct {
ID uint
Name string
}
func main() {
database.InitDB()
_ = database.DB.AutoMigrate(&User{})
// Write (goes to master)
database.DB.Create(&User{Name: "Alice"})
// Normal read (goes to replica)
var user User
database.DB.First(&user, "name = ?", "Alice")
fmt.Println("Read user:", user)
// Force master for read
database.DB.Clauses(dbresolver.Write).First(&user, "name = ?", "Alice")
fmt.Println("Forced master read:", user)
// Transaction (all operations use master)
database.DB.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Name: "Bob"})
var u User
tx.First(&u, "name = ?", "Bob")
fmt.Println("In transaction:", u)
return nil
})
}Production Best Practices
Connection Pool Tuning SetMaxOpenConns ≈ 70% of the DB's max connections. SetMaxIdleConns ≈ CPU cores × 2. SetConnMaxLifetime ≤ MySQL wait_timeout.
Read/Write Strategy
Default reads go to replicas.
Write‑then‑read consistency: force master for the immediate read.
All operations inside a transaction stay on master.
Logging & Monitoring
Enable TraceResolverMode to see which DB a query uses.
Integrate with Prometheus + Grafana to monitor slow queries and connection counts.
Fault Tolerance & Health Checks
Periodically run SELECT 1 on replicas to verify health.
Take unhealthy replicas offline promptly to avoid query failures.
Business‑Scenario Recommendations
High‑consistency workloads (orders, payments) → always use master.
Read‑heavy, write‑light traffic → default to replicas.
Reporting/analytics → dedicated resolver pointing to read‑only clusters.
Conclusion
GORM’s DBResolver plugin enables Go developers to implement read/write splitting and multi‑database management with clear semantics, extensible load‑balancing policies, and guaranteed transaction consistency. Proper configuration allows most production scenarios to run smoothly, improving stability and performance.
💡 Discussion : When replication lag is severe, how would you ensure write‑after‑read consistency? Force master reads or adopt an application‑level tolerance strategy?
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Wrench
Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
