悲观锁实现
GORM 悲观锁通过 Clauses(clause.Locking{Strength: "UPDATE"}) 实现 SELECT FOR UPDATE,锁定查询行直到事务提交。
SELECT FOR UPDATE 语法
基本用法
在事务中使用 Clauses 添加行锁,其他事务读取被锁定行时会阻塞或跳过:
Go
func UpdateWithPessimisticLock(db *gorm.DB, id uint) error {
return db.Transaction(func(tx *gorm.DB) error {
var user User
// SELECT * FROM users WHERE id = ? FOR UPDATE
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
First(&user, id).Error; err != nil {
return err
}
user.Balance -= 100
return tx.Save(&user).Error
})
}
FOR UPDATE会锁定选中的行,其他事务的SELECT FOR UPDATE、UPDATE、DELETE操作会被阻塞,直到锁释放。
锁强度选项
| Strength 值 | 说明 | 适用场景 |
|---|---|---|
UPDATE | 标准行锁,阻塞读写 | 需要修改数据的场景 |
SHARE | 共享锁,允许并发读 | 仅读取但需防止修改 |
Go
// 共享锁示例
tx.Clauses(clause.Locking{Strength: "SHARE"}).Find(&users)
// NOWAIT 选项:不等待,直接返回错误
tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).First(&user, id)
// SKIP LOCKED 选项:跳过已锁定行
tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).Find(&users)
高并发场景应用
库存扣减
Go
func DeductStock(db *gorm.DB, productID uint, quantity int) error {
return db.Transaction(func(tx *gorm.DB) error {
var product Product
// 锁定商品行,防止并发扣减
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
First(&product, productID).Error; err != nil {
return err
}
if product.Stock < quantity {
return errors.New("库存不足")
}
product.Stock -= quantity
return tx.Save(&product).Error
})
}
余额操作
Go
func TransferMoney(db *gorm.DB, fromID, toID uint, amount float64) error {
return db.Transaction(func(tx *gorm.DB) error {
var from, to User
// 按 ID 排序锁定,避免死锁
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id IN (?, ?)", fromID, toID).Order("id ASC").Find(&[]User{}).Error; err != nil {
return err
}
if err := tx.First(&from, fromID).Error; err != nil {
return err
}
if from.Balance < amount {
return errors.New("余额不足")
}
if err := tx.First(&to, toID).Error; err != nil {
return err
}
from.Balance -= amount
to.Balance += amount
tx.Save(&from)
return tx.Save(&to).Error
})
}
多行锁定必须统一顺序(如按 ID 排序),否则不同事务以不同顺序锁定相同行会导致死锁。
注意事项
- 事务必须显式开启:悲观锁仅在事务内生效,自动提交模式下无效
- 锁持有时间尽量短:长事务会导致其他请求长时间阻塞,影响吞吐量
- 死锁风险:多个事务交叉锁定不同行可能产生死锁,数据库会自动检测并回滚其中一个
- 数据库兼容性:
NOWAIT和SKIP LOCKED仅 PostgreSQL/MySQL 8.0+ 支持,SQLite 不支持 - 索引必须命中:无索引的全表扫描会锁定大量行甚至整表,严重影响性能
要点总结
- 使用
Clauses(clause.Locking{Strength: "UPDATE"})实现SELECT FOR UPDATE - 必须在事务内使用,确保数据强一致性
- 多行操作按固定顺序锁定,避免死锁
- 锁粒度越小越好,务必走索引缩小锁定范围
NOWAIT和SKIP LOCKED适合排队和队列场景
存放路径:D:\git2\jwdev\articles\GORM\专家\高级并发与锁机制\悲观锁实现.md
📝 发现内容有误?点击此处直接编辑