GORM 中的嵌套事务与 SavePoint 事务
1. 嵌套事务
嵌套事务是指一个事务中包含另一个事务。例如,在一个数据库操作中,你可能需要在一个主事务中嵌套执行子事务。
GORM 对嵌套事务的支持:
- GORM 中通过
DB.Transaction
方法嵌套事务。 - 内部事务出错时,会回滚到父事务的状态。
- 如果外部事务提交了,嵌套的事务也会生效。
示例:
go
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// 外层事务
err := db.Transaction(func(tx *gorm.DB) error {
// 内层事务
err := tx.Transaction(func(tx2 *gorm.DB) error {
if err := tx2.Create(&User{Name: "Nested Transaction"}).Error; err != nil {
return err // 内层回滚
}
return nil // 内层提交
})
if err != nil {
return err // 外层回滚
}
if err := tx.Create(&User{Name: "Outer Transaction"}).Error; err != nil {
return err // 外层回滚
}
return nil // 外层提交
})
if err != nil {
fmt.Println("事务失败:", err)
} else {
fmt.Println("事务成功")
}
}
type User struct {
ID uint
Name string
}
注意:
- 如果子事务回滚,父事务仍然可以正常执行。
- 子事务的回滚不会影响父事务中的其他操作。
2. SavePoint 事务
SavePoint 是数据库事务的一种控制方式,它允许你在事务中创建检查点(SavePoint),并在发生错误时回滚到特定的检查点,而不是完全回滚整个事务。
GORM 中 SavePoint 的使用:
- 使用
SavePoint
创建检查点。 - 使用
RollbackTo
回滚到某个检查点。
示例:
go
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
tx := db.Begin()
// SavePoint 检查点
tx.SavePoint("sp1")
if err := tx.Create(&User{Name: "First SavePoint"}).Error; err != nil {
tx.RollbackTo("sp1") // 回滚到 sp1
}
// SavePoint 检查点
tx.SavePoint("sp2")
if err := tx.Create(&User{Name: "Second SavePoint"}).Error; err != nil {
tx.RollbackTo("sp2") // 回滚到 sp2
}
// 提交事务
if err := tx.Commit().Error; err != nil {
fmt.Println("提交失败:", err)
} else {
fmt.Println("事务成功提交")
}
}
type User struct {
ID uint
Name string
}
注意:
SavePoint
和RollbackTo
需要底层数据库支持。例如,SQLite 和 MySQL 支持 SavePoint。- 如果底层数据库不支持 SavePoint,使用时会报错。
区别和选择
功能 | 嵌套事务 | SavePoint 事务 |
---|---|---|
实现方式 | 通过 GORM 提供的 Transaction 方法封装。 |
显式创建检查点并回滚到特定检查点。 |
适用场景 | 子事务相对独立,但需要依赖主事务的提交或回滚。 | 需要在事务中多次设置状态点,灵活回滚到某点。 |
复杂度 | 比较简单,直接使用 Transaction 方法嵌套。 |
较复杂,需要手动管理 SavePoint 和回滚点。 |
以下是一个企业级案例,一个电商平台需要创建订单并更新库存,同时提供对操作失败的容错机制。
案例背景
-
场景描述:
- 用户提交订单时,需要:
- 在数据库中创建订单。
- 扣减库存。
- 记录操作日志。
- 如果任意一步失败,需要保证事务的一致性。
- 用户提交订单时,需要:
-
技术需求:
- 使用嵌套事务确保不同模块的独立性。
- 使用 SavePoint 处理中间步骤的可回滚。
-
数据库表结构:
orders
表:存储订单信息。products
表:存储商品库存。logs
表:记录操作日志。
代码实现
go
package main
import (
"errors"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint // 用户ID
ProductID uint // 商品ID
Quantity int // 购买数量
Status string // 订单状态
}
type Product struct {
ID uint `gorm:"primaryKey"`
Name string
Stock int // 库存数量
}
type Log struct {
ID uint `gorm:"primaryKey"`
Message string // 日志信息
}
func main() {
// 初始化数据库连接(假设使用 MySQL)
dsn := "user:password@tcp(127.0.0.1:3306)/ecommerce?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败")
}
// 自动迁移(仅用于演示,实际生产中建议手动管理表结构)
db.AutoMigrate(&Order{}, &Product{}, &Log{})
// 调用企业级事务流程
err = createOrder(db, 1, 1, 2) // 用户 1 购买商品 1,购买数量为 2
if err != nil {
fmt.Println("订单创建失败:", err)
} else {
fmt.Println("订单创建成功")
}
}
func createOrder(db *gorm.DB, userID, productID, quantity int) error {
return db.Transaction(func(tx *gorm.DB) error {
// 创建 SavePoint 检查点
tx.SavePoint("start_order")
// Step 1: 扣减库存
if err := reduceStock(tx, productID, quantity); err != nil {
tx.RollbackTo("start_order")
return err
}
// Step 2: 创建订单
order := Order{
UserID: uint(userID),
ProductID: uint(productID),
Quantity: quantity,
Status: "Created",
}
if err := tx.Create(&order).Error; err != nil {
tx.RollbackTo("start_order")
return err
}
// Step 3: 记录日志
log := Log{
Message: fmt.Sprintf("用户 %d 创建订单 %d", userID, order.ID),
}
if err := tx.Create(&log).Error; err != nil {
tx.RollbackTo("start_order")
return err
}
// 提交事务
return nil
})
}
func reduceStock(tx *gorm.DB, productID, quantity int) error {
var product Product
if err := tx.First(&product, productID).Error; err != nil {
return errors.New("商品不存在")
}
if product.Stock < quantity {
return errors.New("库存不足")
}
// 扣减库存
product.Stock -= quantity
if err := tx.Save(&product).Error; err != nil {
return errors.New("库存更新失败")
}
return nil
}
功能讲解
-
事务嵌套:
- 订单创建逻辑封装在
createOrder
函数中,库存更新 (reduceStock
) 是一个嵌套的子操作。 db.Transaction
保证了reduceStock
和日志记录的原子性。
- 订单创建逻辑封装在
-
SavePoint 使用:
- 在事务开始时设置检查点
SavePoint("start_order")
。 - 任意步骤失败时,通过
RollbackTo
回滚到检查点,避免影响全局事务。
- 在事务开始时设置检查点
-
模块化设计:
- 每个业务逻辑拆分为独立的函数,如
reduceStock
和日志记录。 - 确保代码可读性和可维护性。
- 每个业务逻辑拆分为独立的函数,如
-
数据库层的幂等性:
- 通过事务机制保证多次调用不会产生脏数据。
- 在高并发场景下可结合数据库锁进一步优化。
扩展建议
-
并发控制:
- 针对库存扣减,可以结合数据库的乐观锁或悲观锁机制,避免超卖。
-
日志的异步化:
- 日志记录可以通过消息队列(如 Kafka 或 RabbitMQ)异步处理,减少事务时长。
-
监控和告警:
- 在生产环境中,可通过 APM 工具(如 Prometheus 或 Jaeger)监控事务耗时和失败情况。
练习案例
go
package _case
import (
"gorm.io/gorm"
"log"
)
// Transaction 事务
func Transaction() {
t := Teacher{
Name: "nick",
Age: 40,
Salary: 12345.123,
Email: "nick@gmail.com",
}
c := Course{
Name: "golang",
Price: 12345.1234,
}
DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&t).Error; err != nil {
return err
}
c.UserID = t.ID
if err := tx.Create(&c).Error; err != nil {
return err
}
return nil
})
}
// NestTransaction 嵌套事务指的是再一个外部事务内启动一或者多个内部事务,
// 每个内部事务可以独立回滚
// 外部事务提交成功,内部事务才能成功
// 内部事务失败,不影响其他同级或者高级事务,但是外部事物回滚,所有内部也会回滚
// 嵌套事务不支持预编译(Prepared Statements)
func NestTransaction() {
t := Teacher{
Name: "nick",
Age: 40,
Salary: 12345.123,
Email: "nick@gmail.com",
}
t1 := Teacher{
Name: "king",
Age: 40,
Salary: 12345.123,
Email: "nick@gmail.com",
}
t2 := Teacher{
Name: "mark",
Age: 40,
Salary: 12345.123,
Email: "nick@gmail.com",
}
DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&t).Error; err != nil {
return err
}
tx.Transaction(func(tx1 *gorm.DB) error {
if err := tx1.Create(&t1).Error; err != nil {
return err
}
return nil
})
tx.Transaction(func(tx2 *gorm.DB) error {
if err := tx2.Create(&t2).Error; err != nil {
return err
}
return nil
})
return nil
})
}
// ManualTransaction 手动事务,模拟平常手写事务
func ManualTransaction() {
t := Teacher{
Name: "nick",
Age: 40,
Salary: 12345.123,
Email: "nick@gmail.com",
}
c := Course{
Name: "golang",
Price: 12345.1234,
}
tx := DB.Begin()
defer func() { // 确保在发生未处理的 panic 时,能够及时回滚事务,避免数据不一致的问题。
if err := recover(); err != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
log.Fatalln(tx.Error)
return
}
if err := tx.Create(&t).Error; err != nil {
tx.Rollback()
return
}
c.UserID = t.ID
if err := tx.Create(&c).Error; err != nil {
tx.Rollback()
return
}
tx.Commit()
}
// SavePointTransaction 保存点事务,即事务中可以创建多个保存点,可以回滚到指定保存点
func SavePointTransaction() {
t := Teacher{
Name: "nick",
Age: 40,
Salary: 12345.123,
Email: "nick@0voice.com",
}
c := Course{
Name: "golang 云原生",
Price: 12345.1234,
}
tx := DB.Begin()
defer func() {
if err := recover(); err != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
log.Fatal(tx.Error)
return
}
if err := tx.Create(&t).Error; err != nil {
tx.Rollback()
return
}
tx.SavePoint("teacher") // SavePoint 设置回滚点
c.UserID = t.ID
if err := tx.Create(&c).Error; err != nil {
tx.RollbackTo("teacher") // 指定回滚到
return
}
tx.Commit()
}