vault backup: 2026-04-28 20:56:51

This commit is contained in:
2026-04-28 20:56:51 +08:00
parent 39aaa384ba
commit 0cc894a3cb
14 changed files with 1566 additions and 440 deletions
+134 -5
View File
@@ -121,20 +121,37 @@ func GetUserWithTopOrders(db *gorm.DB, id uint) (*User, error) {
> 不同数据库对 IN 列表大小有限制:MySQL 默认 `max_allowed_packet` 约 4MB,超过会被截断或报错。对于大规模预加载,考虑分批查询:
> ```go
> func preloadInBatches(db *gorm.DB, ids []uint, dest any) error {
> // dest 必须是切片类型的指针,如 *[]Profile
> batch := 500
> slices := reflect.ValueOf(dest).Elem() // 获取切片本身
>
> for i := 0; i < len(ids); i += batch {
> end := i + batch
> if end > len(ids) {
> end = len(ids)
> }
> end := min(i+batch, len(ids))
> chunk := ids[i:end]
> if err := db.Where("user_id IN ?", chunk).Find(dest).Error; err != nil {
>
> var batchResult []map[string]any
> if err := db.Where("user_id IN ?", chunk).Find(&batchResult).Error; err != nil {
> return err
> }
>
> // 追加到原有切片中
> for _, item := range batchResult {
> val := reflect.New(slices.Type().Elem())
> // 将 map 赋值到结构体字段...(此处略)
> slices.Set(reflect.Append(slices, val.Elem()))
> }
> }
> return nil
> }
> ```
>
> > [!tip] 更简单的替代方案
> > 如果关联数据量可控,直接用 Preload + Limit 限制每条主记录的关联数量,避免大规模 IN 查询:
> > ```go
> > db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
> > return db.Order("created_at DESC").Limit(20)
> > }).Find(&users)
> > ```
## 减少不必要的 SELECT
@@ -163,6 +180,46 @@ var stat UserCount
db.Model(&User{}).Select("COUNT(*) as count").Scan(&stat)
```
## 批量操作替代循环
### CreateInBatches
```go
// ❌ 逐条插入:N 次 INSERT
for _, user := range users {
db.Create(&user)
}
// ✅ 批量插入:单次 SQL(底层拆分为 INSERT INTO ... VALUES (...), (...), (...))
users := []User{{Name: "A"}, {Name: "B"}, {Name: "C"}}
db.CreateInBatches(users, 100) // 每批 100 条,共 3 条 SQL
```
> [!tip] 默认批次大小
> `CreateInBatches` 的第二个参数是**批次大小**而非总数。如果省略或用 0,GORM 会按模型切片长度自动决定——全量一次性插入可能超出 `max_allowed_packet`。建议显式指定 100~500。
### 批量更新与删除
```go
// 批量更新:使用 SELECT + CASE WHEN 实现多行不同值更新
db.Model(&Product{}).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sku"}},
DoUpdates: clause.AssignmentColumns([]string{"price", "stock"}),
}).Create(&products)
// UPSERT 语义:存在则更新,不存在则插入
// 批量删除(条件一致时)
db.Where("status = ?", "cancelled").Delete(&Order{})
// DELETE FROM orders WHERE status = 'cancelled';
```
> [!note] 批量 vs 单条的性能对比
> | 操作 | 1000 条记录 | 说明 |
> |------|-----------|------|
> | 逐条 Create | ~800ms | N 次网络往返 + N 次事务提交 |
> | CreateInBatches(500) | ~50ms | 2 条 SQL,2 次提交 |
> | Raw SQL Batch | ~30ms | 绕过 ORM 映射,最快但有维护成本 |
## 启用预编译语句缓存
```go
@@ -177,6 +234,38 @@ db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
> [!note] 缓存机制
> GORM 内置一个基于 `sync.Map` 的简单缓存,存储已经编译好的 SQL 语句。适合重复执行的固定模式查询(如根据 ID 查用户)。不适合大量参数不同的动态查询——缓存命中率低会浪费内存。
## 连接池配置
```go
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sqlDB, _ := db.DB() // 获取底层 *sql.DB
// ⚙️ 连接池调优
sqlDB.SetMaxOpenConns(50) // 最大打开连接数(含使用中 + 空闲)
sqlDB.SetMaxIdleConns(25) // 最大空闲连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间,防 MySQL wait_timeout 断开
sqlDB.SetConnMaxIdleTime(30 * time.Minute) // Go 1.15+,空闲超时主动回收
```
> [!warning] 常见陷阱
> - **默认 MaxOpenConns = 0(无限制)**:高并发时可能瞬间创建数百连接,压垮数据库。务必显式设置!
> - **PreparedStmt 缓存不计入连接池**:开启 `PrepareStmt: true` 后,GORM 使用独立的缓存结构,但 `.DB()` 返回的还是底层连接池。
> - **连接泄漏**:事务忘记 Commit/Rollback 会占用连接。配合前面提到的「缩小事务粒度」一起使用效果最佳。
> [!tip] 如何确定合适的连接池大小?
> ```
> 推荐公式: MaxOpenConns = CPU核心数 × 2 + 磁盘数
> 例: 4核 2盘 → 4×2+2 = 10 (I/O 密集型可适当放宽到 20~50)
> ```
> 更准确的方式是通过压测观察数据库的活跃连接数和等待队列。
## 索引优化建议
### 在 tag 中声明索引
@@ -275,6 +364,46 @@ func HandleRequest() error {
> })
> ```
## 软删除的性能陷阱
GORM 的 `SoftDelete` 功能非常便捷,但在高频查询场景下会带来隐式开销。
### 自动注入 WHERE deleted_at IS NULL
```go
type User struct {
gorm.Model // 包含 DeletedAt *time.Time
}
// 无论你是否主动写条件,GORM 自动附加:
db.Where("status = ?", "active").Find(&users)
// 实际执行:SELECT * FROM users WHERE status = 'active' AND deleted_at IS NULL;
```
这意味着**每一个查询都多了一个索引列的判断**。
### 优化策略
```go
// ❌ 带软删除字段的表,WHERE 子句变复杂时走不了覆盖索引
// ✅ 策略一:为 (deleted_at, 查询字段) 建复合索引
type Article struct {
ID uint `gorm:"primaryKey"`
Status string `gorm:"size:16;not null;index:idx_status_deleted"`
DeletedAt time.Time `gorm:"index:idx_status_deleted"`
}
// ✅ 策略二:不需要软删除的场景用 Unscoped 跳过过滤
db.Unscoped().Where("id = ?", 1).First(&user)
// SELECT * FROM users WHERE id = 1; (不含 deleted_at 条件)
// ✅ 策略三:归档历史数据到独立表,避免主表膨胀
// archived_users 表不设软删除,定期将旧数据迁移过去
```
> [!danger] 数据量敏感
> 当单表超过千万级记录且开启软删除时,未加合适索引的查询会从 index scan 退化为全表扫描(需要扫描并判断每一行的 `deleted_at` 值)。务必用 `EXPLAIN` 验证核心查询路径。
## 性能优化检查清单
```mermaid