性能优化


第十章:性能优化

10.1 连接池优化

核心参数

sqlDB, err := db.DB()
if err != nil {
    log.Fatal(err)
}

// 最大空闲连接数(建议:连接数 / 2)
sqlDB.SetMaxIdleConns(25)

// 最大打开连接数(建议:连接数)
sqlDB.SetMaxOpenConns(50)

// 连接最大生命周期(防止连接过期)
sqlDB.SetConnMaxLifetime(30 * time.Minute)

// 连接最大空闲时间(Go 1.15+)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)

不同场景推荐配置

场景 MaxIdleConns MaxOpenConns ConnMaxLifetime
低并发 Web 10-25 25-50 30m
高并发 API 50-100 100-200 15m
批处理任务 5-10 20-30 60m
微服务 5-15 20-40 30m

10.2 查询优化

只查询需要的字段

// 差:SELECT *
db.Find(&users)

// 好:只选需要的字段
db.Select("id", "name", "email").Find(&users)

// 更好:使用特定结构体
type UserListItem struct {
    ID   uint
    Name string
}
var items []UserListItem
db.Model(&User{}).Find(&items)

使用 Limit

// 分页必须加 Limit
db.Offset(0).Limit(100).Find(&users)

// 列表页限制最大数量
db.Limit(1000).Find(&users)

避免 N+1 查询

// 差:N+1
var users []User
db.Find(&users)
for _, u := range users {
    db.Model(&u).Related(&u.Orders)  // N 次查询
}

// 好:使用 Preload
db.Preload("Orders").Find(&users)  // 2 次查询

// 更好:使用 Join(数据量小时)
db.Joins("Orders").Find(&users)  // 1 次查询

10.3 预加载策略

控制预加载数量

// 限制关联数据数量
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("created_at DESC").Limit(10)
}).Find(&users)

条件预加载

// 只预加载有效订单
db.Preload("Orders", "status = ?", OrderStatusActive).Find(&users)

// 多层预加载优化
db.Preload("Orders.Items", func(db *gorm.DB) *gorm.DB {
    return db.Select("id", "order_id", "product_name", "price")
}).Find(&users)

10.4 批量操作

批量插入

// 差:逐条插入
for _, user := range users {
    db.Create(&user)  // N 次 INSERT
}

// 好:批量插入
db.CreateInBatches(users, 100)  // 每 100 条一批

// 更好:原生 SQL 批量插入
db.Exec("INSERT INTO users (name, email) VALUES (?, ?), (?, ?), ...", values...)

批量更新

// 使用 Case When 批量更新
db.Model(&User{}).Where("id IN ?", ids).Update("status", 2)

// 批量 Upsert(MySQL)
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.AssignmentColumns([]string{"name", "age", "updated_at"}),
}).Create(&users)

10.5 缓存策略

一级缓存(应用内缓存)

import "github.com/patrickmn/go-cache"

var cache = cache.New(5*time.Minute, 10*time.Minute)

func GetUserWithCache(db *gorm.DB, id uint) (*User, error) {
    key := fmt.Sprintf("user:%d", id)
    
    if cached, found := cache.Get(key); found {
        return cached.(*User), nil
    }
    
    var user User
    if err := db.First(&user, id).Error; err != nil {
        return nil, err
    }
    
    cache.Set(key, &user, cache.DefaultExpiration)
    return &user, nil
}

查询缓存(配合 Redis)

func QueryWithCache(db *gorm.DB, key string, dest interface{}, queryFunc func(*gorm.DB) error) error {
    // 从 Redis 获取
    if data, err := redis.Get(key).Result(); err == nil {
        return json.Unmarshal([]byte(data), dest)
    }
    
    // 查询数据库
    if err := queryFunc(db); err != nil {
        return err
    }
    
    // 写入缓存
    if data, err := json.Marshal(dest); err == nil {
        redis.Set(key, data, 5*time.Minute)
    }
    
    return nil
}

10.6 索引优化

合理创建索引

type User struct {
    ID        uint   `gorm:"primaryKey"`
    Email     string `gorm:"uniqueIndex"`  // 唯一查询
    Name      string `gorm:"index"`        // 普通查询
    Status    int    `gorm:"index:idx_status_created"`  // 复合索引前缀
    CreatedAt int64  `gorm:"index:idx_status_created"`  // 复合索引
}

索引使用原则

  1. 高选择性字段建索引(如 email、手机号)
  2. 低选择性字段不建索引(如 status=0/1)
  3. 经常一起查询的字段建复合索引
  4. ORDER BY / GROUP BY 字段建索引

10.7 上下文与超时

// 查询超时控制
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

if err := db.WithContext(ctx).Find(&users).Error; err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        // 查询超时处理
    }
}

// 慢查询阈值
newLogger := logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    logger.Config{
        SlowThreshold: 100 * time.Millisecond,  // 慢查询阈值
        LogLevel:      logger.Warn,
    },
)

10.8 Prepared Statement

// 默认启用,可禁用
// MySQL 需要设置 interpolateParams=false

dsn := "user:pass@tcp(localhost:3306)/db?charset=utf8mb4&parseTime=True&loc=Local&interpolateParams=false"

// GORM 自动缓存 Prepared Statement
// 可通过配置控制
sqlDB, _ := db.DB()
sqlDB.SetConnMaxLifetime(time.Hour)  // 避免连接过期导致 Statement 失效

10.9 读写分离

import "gorm.io/plugin/dbresolver"

db.Use(dbresolver.Register(dbresolver.Config{
    Sources: []gorm.Dialector{mysql.Open("write_dsn")},
    Replicas: []gorm.Dialector{
        mysql.Open("read_dsn_1"),
        mysql.Open("read_dsn_2"),
    },
    Policy: dbresolver.RandomPolicy{},
}))

// 自动路由:写操作走 Sources,读操作走 Replicas
db.Create(&user)    // 写
db.Find(&users)    // 读

10.10 性能监控

Prometheus 监控

import "github.com/wei840222/gorm-prom"

db.Use(gormprom.New(gormprom.Config{
    DBName: "mydb",
}))

自定义监控

type QueryMetrics struct {
    QueryCount    prometheus.Counter
    QueryDuration prometheus.Histogram
}

func (m *QueryMetrics) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
    sql, rows := fc()
    duration := time.Since(begin)
    
    m.QueryCount.Inc()
    m.QueryDuration.Observe(duration.Seconds())
    
    if duration > 100*time.Millisecond {
        log.Printf("慢查询: %s, 耗时: %v", sql, duration)
    }
}

10.11 性能测试

func BenchmarkCreate(b *testing.B) {
    db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    db.AutoMigrate(&User{})
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        db.Create(&User{Name: "test"})
    }
}

func BenchmarkBatchCreate(b *testing.B) {
    db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    db.AutoMigrate(&User{})
    
    users := make([]User, 100)
    for i := range users {
        users[i] = User{Name: fmt.Sprintf("test%d", i)}
    }
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        db.CreateInBatches(users, 100)
    }
}

10.12 实战:性能优化检查清单

// 优化前检查项
type PerformanceChecklist struct {
    ConnectionPool   bool  // 连接池配置是否合理
    IndexUsage       bool  // 查询是否使用索引
    NPlus1           bool  // 是否存在 N+1 问题
    SelectFields     bool  // 是否只查询必要字段
    BatchSize        bool  // 批量操作批次大小
    CacheEnabled     bool  // 热点数据是否缓存
    TimeoutSet       bool  // 是否设置查询超时
    SlowQueryLog     bool  // 是否启用慢查询日志
}

10.13 练习题

  1. 使用 pprof 分析一个 GORM 应用的性能瓶颈
  2. 实现一个带本地缓存的用户查询,对比缓存前后的 QPS
  3. 编写批量插入 100 万条记录的优化方案

10.14 小结

本章从连接池、查询、预加载、批量操作、缓存等方面讲解了 GORM 性能优化技巧。性能优化是一个持续过程,需要结合监控数据和实际场景进行调整。


本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo