乐观锁实现
GORM 乐观锁通过在数据表中添加版本号字段,在更新时校验版本号是否变化来检测冲突,适合读多写少、冲突概率低的场景。
版本号机制
表结构设计
数据表需包含 version 字段,每次更新时递增:
Go
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
Stock int
Version int `gorm:"default:1"` // 乐观锁版本号
}
手动实现乐观锁
通过 WHERE 条件校验版本号,确保更新时版本一致:
Go
func UpdateStockWithOptimisticLock(db *gorm.DB, id uint, newStock int) error {
return db.Transaction(func(tx *gorm.DB) error {
var product Product
if err := tx.First(&product, id).Error; err != nil {
return err
}
oldVersion := product.Version
product.Stock = newStock
product.Version++
// UPDATE products SET stock=?, version=? WHERE id=? AND version=?
result := tx.Model(&product).
Where("version = ?", oldVersion).
Updates(map[string]interface{}{
"stock": product.Stock,
"version": product.Version,
})
if result.Error != nil {
return result.Error
}
// 影响行数为 0 说明版本号已变化,存在并发冲突
if result.RowsAffected == 0 {
return errors.New("乐观锁冲突:数据已被其他事务修改,请重试")
}
return nil
})
}
检查
RowsAffected == 0是乐观锁的核心判断逻辑,说明查询到的版本号与当前数据库不一致。
重试机制
乐观锁冲突时应由业务层重试,而非直接报错:
Go
func UpdateWithRetry(db *gorm.DB, maxRetries int, id uint, newStock int) error {
for i := 0; i < maxRetries; i++ {
err := UpdateStockWithOptimisticLock(db, id, newStock)
if err == nil {
return nil
}
// 非乐观锁冲突错误,直接返回
if !strings.Contains(err.Error(), "乐观锁冲突") {
return err
}
// 指数退避重试
time.Sleep(time.Duration(i+1) * 50 * time.Millisecond)
}
return fmt.Errorf("更新失败,已达最大重试次数 %d", maxRetries)
}
时间戳方案
除版本号外,也可使用 updated_at 时间戳实现乐观锁:
Go
type Order struct {
ID uint `gorm:"primaryKey"`
Status string
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func UpdateOrderWithTimestamp(db *gorm.DB, id uint, newStatus string) error {
var order Order
if err := db.First(&order, id).Error; err != nil {
return err
}
oldUpdatedAt := order.UpdatedAt
result := db.Model(&Order{}).
Where("id = ? AND updated_at = ?", id, oldUpdatedAt).
Update("status", newStatus)
if result.RowsAffected == 0 {
return errors.New("数据已被修改")
}
return result.Error
}
适用场景对比
| 锁类型 | 冲突成本 | 适用场景 | 性能特点 |
|---|---|---|---|
| 乐观锁 | 低冲突时高 | 读多写少 | 无锁等待,吞吐量高 |
| 悲观锁 | 高冲突时低 | 写多或强一致 | 行阻塞,并发度受限 |
注意事项
- 冲突检测而非预防:乐观锁不阻止并发读写,仅在更新时检测冲突
- 必须配合重试:冲突后应重新读取最新数据并重试,而非直接失败
- 仅保护单行:多行更新无法通过单一版本号保证原子性
- 版本字段类型:推荐使用
int自增,避免时间戳精度问题导致误判 - GORM 插件支持:
gorm.io/plugin/optimistic提供插件自动实现,减少手动编码
要点总结
- 通过版本号字段 +
WHERE version = ?条件更新实现乐观锁 - 使用
RowsAffected == 0判断并发冲突 - 业务层必须实现重试机制,配合指数退避策略
- 适合低冲突场景,高冲突下性能反而不如悲观锁
- 时间戳可作为版本号的替代方案,但精度需注意
存放路径:D:\git2\jwdev\articles\GORM\专家\高级并发与锁机制\乐观锁实现.md
📝 发现内容有误?点击此处直接编辑