性能优化
第十章:性能优化
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"` // 复合索引
}
索引使用原则
- 高选择性字段建索引(如 email、手机号)
- 低选择性字段不建索引(如 status=0/1)
- 经常一起查询的字段建复合索引
- 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 练习题
- 使用 pprof 分析一个 GORM 应用的性能瓶颈
- 实现一个带本地缓存的用户查询,对比缓存前后的 QPS
- 编写批量插入 100 万条记录的优化方案
10.14 小结
本章从连接池、查询、预加载、批量操作、缓存等方面讲解了 GORM 性能优化技巧。性能优化是一个持续过程,需要结合监控数据和实际场景进行调整。
本文代码地址:https://github.com/LittleMoreInteresting/gorm_study
欢迎关注公众号,一起学习进步!