代码写好了,要上线了。生产环境和开发环境差别很大,这一章聊聊 GORM 在生产环境的配置和注意事项。
生产环境的数据库连接要考虑高可用、安全、性能。
连接字符串
不要把密码硬编码在代码里:
// 不好
dsn := "root:password123@tcp(localhost:3306)/mydb"
// 好:从环境变量读取
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
log.Fatal("DATABASE_URL is required")
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
连接参数
MySQL 连接推荐参数:
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s&readTimeout=10s&writeTimeout=10s",
os.Getenv("DB_USER"),
os.Getenv("DB_PASS"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_NAME"),
)
关键参数:
charset=utf8mb4:支持完整的 UTF-8,包括 emojiparseTime=True:自动解析时间类型loc=Local:时区设置timeout:连接超时readTimeout/writeTimeout:读写超时生产环境必须配置连接池:
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}
// 根据实际负载调整
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
配置原则:
生产环境不要打印所有 SQL:
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn, // 只记录慢查询和错误
Colorful: false,
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
集成项目的日志框架:
type GormLogger struct {
*zap.SugaredLogger
}
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if err != nil {
l.Errorw("sql error", "error", err, "sql", fc)
}
}
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: &GormLogger{SugaredLogger: zap.L().Sugar()},
})
提供数据库健康检查接口:
func HealthCheck(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
sqlDB, err := db.DB()
if err != nil {
c.JSON(500, gin.H{"status": "unhealthy", "error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
if err := sqlDB.PingContext(ctx); err != nil {
c.JSON(500, gin.H{"status": "unhealthy", "error": err.Error()})
return
}
c.JSON(200, gin.H{"status": "healthy"})
}
}
应用退出时要正确关闭连接:
func main() {
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sqlDB, _ := db.DB()
// 注册关闭钩子
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
log.Println("shutting down...")
// 给正在处理的请求一些时间
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 关闭数据库连接
if err := sqlDB.Close(); err != nil {
log.Printf("error closing database: %v", err)
}
log.Println("database connections closed")
os.Exit(0)
}()
// 启动服务...
}
生产环境要有完善的错误处理:
func (s *UserService) GetUser(id uint) (*User, error) {
var user User
err := s.db.First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &AppError{Code: 404, Message: "用户不存在"}
}
// 记录错误日志
log.Printf("database error: %v", err)
return nil, &AppError{Code: 500, Message: "系统错误"}
}
return &user, nil
}
数据库临时故障时可以重试:
func withRetry(db *gorm.DB, fn func(*gorm.DB) error, maxRetries int) error {
var err error
for i := 0; i < maxRetries; i++ {
err = fn(db)
if err == nil {
return nil
}
// 判断是否可重试的错误
if !isRetryableError(err) {
return err
}
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
}
return err
}
func isRetryableError(err error) bool {
// 连接错误、超时等可以重试
return strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout")
}
读写分离配置:
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
Replicas: []*gorm.DB{
gorm.Open(mysql.Open(replicaDSN1), &gorm.Config{}),
gorm.Open(mysql.Open(replicaDSN2), &gorm.Config{}),
},
})
或者用 dbresolver 插件:
import "gorm.io/plugin/dbresolver"
db.Use(dbresolver.Register(dbresolver.Config{
Replicas: []gorm.Dialector{
mysql.Open(replicaDSN1),
mysql.Open(replicaDSN2),
},
Policy: dbresolver.RandomPolicy{},
}))
// 读操作走从库
db.ReadDB().Find(&users)
// 写操作走主库
db.WriteDB().Create(&user)
暴露数据库相关指标:
func collectDBMetrics(db *gorm.DB) {
sqlDB, _ := db.DB()
stats := sqlDB.Stats()
metrics.SetGauge("db_open_connections", float64(stats.OpenConnections))
metrics.SetGauge("db_in_use", float64(stats.InUse))
metrics.SetGauge("db_idle", float64(stats.Idle))
metrics.SetGauge("db_wait_count", float64(stats.WaitCount))
metrics.SetGauge("db_wait_duration_ms", float64(stats.WaitDuration.Milliseconds()))
}
最小权限原则
应用账号只给必要的权限:
CREATE USER 'app_user'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
-- 不要给 DROP, ALTER 等权限
敏感信息加密
敏感字段加密存储:
type User struct {
ID uint
Name string
Phone string `gorm:"type:varchar(255)"` // 加密存储
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.Phone = encrypt(u.Phone)
return nil
}
func (u *User) AfterFind(tx *gorm.DB) error {
u.Phone = decrypt(u.Phone)
return nil
}
生产环境配置要点:
生产环境容不得马虎,每个细节都要考虑到。