保存点(SavePoint)是事务中的标记点。可以回滚到保存点,而不是回滚整个事务。这提供了更精细的事务控制。
事务通常只能全部提交或全部回滚。保存点允许部分回滚:
BEGIN
操作1
SAVEPOINT sp1
操作2
SAVEPOINT sp2
操作3
ROLLBACK TO sp1 -- 回滚到 sp1,操作2和3被撤销,操作1保留
COMMIT
创建保存点:
tx := db.Begin()
tx.Create(&user)
tx.SavePoint("sp1")
tx.Create(&order)
if someCondition {
tx.RollbackTo("sp1")
}
tx.Commit()
保存点需要命名,用于后续回滚:
tx.SavePoint("before_order")
tx.RollbackTo("before_order")
命名要有意义,方便理解代码意图。
func ProcessData(db *gorm.DB) error {
tx := db.Begin()
defer tx.Rollback()
var user User
tx.First(&user, 1)
tx.SavePoint("before_update")
user.Name = "新名字"
if err := tx.Save(&user).Error; err != nil {
tx.RollbackTo("before_update")
}
tx.SavePoint("before_order")
order := Order{UserID: user.ID}
if err := tx.Create(&order).Error; err != nil {
tx.RollbackTo("before_order")
}
return tx.Commit().Error
}
GORM 的嵌套事务内部就是用保存点实现:
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user)
// 内层事务
return tx.Transaction(func(tx2 *gorm.DB) error {
// 这里实际创建了保存点
tx2.Create(&order)
return nil // 释放保存点
})
})
内层事务失败会回滚到保存点,外层事务继续。
根据条件决定是否回滚:
func CreateOrderWithCheck(db *gorm.DB, order Order) error {
tx := db.Begin()
defer tx.Rollback()
tx.SavePoint("after_order")
if err := tx.Create(&order).Error; err != nil {
return err
}
for _, item := range order.Items {
tx.SavePoint("before_item_" + strconv.Itoa(int(item.ID)))
var product Product
if err := tx.First(&product, item.ProductID).Error; err != nil {
tx.RollbackTo("before_item_" + strconv.Itoa(int(item.ID)))
continue
}
if product.Stock < item.Quantity {
tx.RollbackTo("before_item_" + strconv.Itoa(int(item.ID)))
continue
}
tx.Model(&product).Update("stock", gorm.Expr("stock - ?", item.Quantity))
}
return tx.Commit().Error
}
封装保存点操作:
type SavePoint struct {
tx *gorm.DB
name string
}
func NewSavePoint(tx *gorm.DB, name string) *SavePoint {
tx.SavePoint(name)
return &SavePoint{tx: tx, name: name}
}
func (sp *SavePoint) Rollback() {
sp.tx.RollbackTo(sp.name)
}
func (sp *SavePoint) Release() {
// MySQL 不支持 RELEASE SAVEPOINT,GORM 也不提供
// 保存点会在事务结束时自动释放
}
// 使用
func ProcessOrder(db *gorm.DB) error {
tx := db.Begin()
defer tx.Rollback()
sp := NewSavePoint(tx, "before_process")
if err := doSomething(tx); err != nil {
sp.Rollback()
}
return tx.Commit().Error
}
批量导入容错
func ImportUsers(db *gorm.DB, users []User) (int, error) {
tx := db.Begin()
defer tx.Rollback()
successCount := 0
for i, user := range users {
spName := fmt.Sprintf("user_%d", i)
tx.SavePoint(spName)
if err := tx.Create(&user).Error; err != nil {
tx.RollbackTo(spName)
log.Printf("导入用户 %s 失败: %v", user.Name, err)
continue
}
successCount++
}
return successCount, tx.Commit().Error
}
多步骤流程
func ProcessPayment(db *gorm.DB, payment Payment) error {
tx := db.Begin()
defer tx.Rollback()
tx.SavePoint("start")
if err := tx.Create(&payment).Error; err != nil {
return err
}
tx.SavePoint("after_payment")
var order Order
if err := tx.First(&order, payment.OrderID).Error; err != nil {
tx.RollbackTo("start")
return err
}
order.Status = "paid"
if err := tx.Save(&order).Error; err != nil {
tx.RollbackTo("after_payment")
return err
}
tx.SavePoint("after_order")
notification := Notification{
UserID: order.UserID,
Content: "订单已支付",
}
if err := tx.Create(¬ification).Error; err != nil {
tx.RollbackTo("after_order")
log.Printf("通知创建失败: %v", err)
}
return tx.Commit().Error
}
保存点数量
保存点会占用资源,不要创建过多。用完及时释放(事务提交或回滚)。
命名冲突
同名保存点会被覆盖:
tx.SavePoint("sp1")
tx.Create(&user)
tx.SavePoint("sp1") // 覆盖之前的 sp1
tx.Create(&order)
tx.RollbackTo("sp1") // 回到第二个 sp1,order 被撤销
数据库支持
主流数据库都支持保存点,但语法略有差异。GORM 做了统一封装。
保存点提供了事务内的部分回滚能力。合理使用可以处理复杂业务流程,实现容错机制。注意保存点的命名和数量控制。