Gorm(十二)乐观锁和悲观锁
在 Gorm 中,乐观锁、悲观锁用于解决并发场景下的数据一致性问题,而 Context 则用于控制数据库操作的超时和取消,三者结合可构建健壮的并发处理逻辑。以下是详细说明:
一、乐观锁(Optimistic Locking)
乐观锁假设并发操作不会频繁冲突,通过版本号(或时间戳)机制实现,仅在提交时检查数据是否被修改,适合冲突较少的场景(如读多写少)。
实现方式
- 模型定义:在结构体中添加
gorm:"optimistic_lock"标签的版本字段(通常为Version int)。 - 原理:更新时会自动检查版本号,若版本号与数据库一致则更新(并自增版本),否则视为数据已被修改,返回错误。
示例
type Product struct {gorm.ModelName stringStock intVersion int `gorm:"optimistic_lock"` // 乐观锁版本字段
}// 并发扣减库存
func deductStock(db *gorm.DB, productID, num int) error {var product Product// 1. 查询商品(获取当前版本)if err := db.First(&product, productID).Error; err != nil {return err}// 2. 检查库存if product.Stock < num {return fmt.Errorf("库存不足")}// 3. 更新库存(Gorm 自动添加 WHERE version = ? 条件)product.Stock -= numresult := db.Save(&product)if result.Error != nil {return result.Error // 版本不一致时,返回错误(如:record not found)}if result.RowsAffected == 0 {return fmt.Errorf("并发冲突,库存已被修改")}return nil
}
生成的 SQL:
UPDATE products SET stock = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?
- 若版本匹配,更新成功,版本号自增。
- 若版本不匹配(其他事务已修改),
RowsAffected为 0,需业务层重试或提示用户。
特点
- 无锁阻塞:不锁定数据,并发性能高。
- 冲突处理:冲突时需业务层手动重试(如通过循环重试)。
- 适用场景:读多写少、冲突频率低的场景(如商品详情页库存更新)。
二、悲观锁(Pessimistic Locking)
悲观锁假设并发操作会频繁冲突,通过数据库的 FOR UPDATE 语句锁定记录,阻止其他事务修改,直到当前事务完成,适合冲突频繁的场景(如秒杀、抢票)。
实现方式
通过 Clauses(clause.Locking{Strength: "UPDATE"}) 或直接在 SQL 中添加 FOR UPDATE 实现,查询时锁定记录。
示例
// 悲观锁扣减库存
func deductStockPessimistic(db *gorm.DB, productID, num int) error {// 1. 查询并锁定商品(FOR UPDATE)var product Producttx := db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, productID)if tx.Error != nil {return tx.Error}// 2. 检查库存if product.Stock < num {return fmt.Errorf("库存不足")}// 3. 更新库存(此时记录已被锁定,其他事务需等待)product.Stock -= numreturn db.Save(&product).Error
}
生成的 SQL:
SELECT * FROM products WHERE id = ? FOR UPDATE;(锁定记录)
UPDATE products SET stock = ?, updated_at = ? WHERE id = ?;
特点
- 阻塞性:锁定期间其他事务无法修改记录,可能导致等待超时。
- 强一致性:确保当前事务操作时数据不被干扰。
- 适用场景:写操作频繁、冲突高的场景(如秒杀系统库存扣减)。
三、Context 超时与取消
Context 用于传递请求的生命周期(如超时、取消信号),Gorm 支持通过 WithContext 方法将 Context 与数据库操作绑定,避免长时间阻塞或资源泄漏。
1. 超时控制
设置操作超时时间,超过时间后自动取消请求,避免数据库连接长期占用。
import ("context""time"
)// 带超时的查询
func queryWithTimeout(db *gorm.DB, userID int) (*User, error) {// 创建 2 秒超时的 Contextctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel() // 确保资源释放var user User// 将 Context 传入 Gorm 操作result := db.WithContext(ctx).First(&user, userID)if result.Error != nil {return nil, result.Error // 超时会返回 context.DeadlineExceeded 错误}return &user, nil
}
2. 取消操作
通过 context.WithCancel 手动取消操作(如用户主动取消请求)。
// 带取消功能的更新
func updateWithCancel(db *gorm.DB, userID int, newName string) error {ctx, cancel := context.WithCancel(context.Background())// 模拟异步取消(如用户点击取消按钮)go func() {time.Sleep(1 * time.Second)cancel() // 1秒后取消操作}()result := db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Update("name", newName)return result.Error // 取消会返回 context.Canceled 错误
}
特点
- 资源控制:防止慢查询或网络问题导致的连接泄漏。
- 兼容性:需数据库驱动支持
Context(如gorm.io/driver/mysql完全支持)。 - 事务集成:事务中也可使用
WithContext,超时或取消会导致事务回滚。
四、对比与最佳实践
| 机制 | 核心逻辑 | 并发性能 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 乐观锁 | 版本号检查,无锁 | 高 | 读多写少,冲突少(如商品详情) | 需处理冲突重试 |
| 悲观锁 | FOR UPDATE 锁定记录 | 中 | 写多读少,冲突多(如秒杀) | 控制事务时长,避免死锁 |
| Context | 超时/取消信号传递 | 无影响 | 所有场景,尤其长耗时操作 | 及时调用 cancel 释放资源 |
组合使用示例
秒杀场景(高冲突+超时控制):
func seckill(db *gorm.DB, productID, userID, num int) error {// 1. 超时控制(防止阻塞过久)ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()// 2. 悲观锁事务(确保库存操作原子性)return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {// 锁定商品记录var product Productif err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, productID).Error; err != nil {return err}// 扣减库存if product.Stock < num {return fmt.Errorf("库存不足")}product.Stock -= numreturn tx.Save(&product).Error})
}
总结
- 乐观锁:通过版本号实现无锁并发,适合低冲突场景,需手动处理冲突。
- 悲观锁:通过
FOR UPDATE锁定记录,适合高冲突场景,需控制事务时长。 - Context:控制操作超时和取消,避免资源泄漏,是所有场景的基础保障。
根据业务的冲突频率和性能需求选择锁机制,并结合 Context 确保系统的健壮性。
